<?php

namespace App\Services;

/**
 * ITSWEBER CMS – Plugin Loader (Phase 17)
 *
 * Loads active plugins at bootstrap time.
 *
 * Active plugins are persisted in storage/plugins_active.json as a JSON array
 * of slug strings, e.g. ["my-plugin", "another-plugin"].
 *
 * Security rules enforced before loading any plugin.php:
 *  1. Slug must pass the whitelist regex [a-z0-9][a-z0-9\-]*
 *  2. Plugin directory must exist under plugins/
 *  3. plugin.php entry-point must be a regular file
 *  4. Realpath of plugin.php must be inside plugins/{slug}/
 *  5. Each plugin.php is loaded in an isolated try/catch – a single broken
 *     plugin cannot abort the entire boot sequence
 *
 * Plugins are loaded in the order they appear in the active list.
 * At load time, plugin.php receives no arguments.  It may call add_hook()
 * and apply_hook() to integrate with the CMS.
 */
class PluginLoader
{
    /** Whitelist pattern for plugin slugs (same as PluginController). */
    private const SLUG_PATTERN = '/^[a-z0-9][a-z0-9\-]*$/';

    /** Path to the active-plugins JSON file, relative to $basePath. */
    private const ACTIVE_FILE = 'storage/plugins_active.json';

    // ─────────────────────────────────────────────────────────────────────────
    // Public API
    // ─────────────────────────────────────────────────────────────────────────

    /**
     * Load all currently active plugins.
     *
     * Called once at bootstrap, after App::init(), before route dispatch.
     *
     * @param string $basePath Absolute path to the CMS root (BASE_PATH)
     */
    public static function loadActive(string $basePath): void
    {
        $activeFile = $basePath . '/' . self::ACTIVE_FILE;

        if (!is_file($activeFile)) {
            return; // No active list yet – nothing to do
        }

        $slugs = self::readActiveFile($activeFile);

        if (empty($slugs)) {
            return;
        }

        $pluginsBase = rtrim($basePath, '/') . '/plugins';

        foreach ($slugs as $slug) {
            self::loadPlugin($pluginsBase, $slug);
        }
    }

    // ─────────────────────────────────────────────────────────────────────────
    // Static helpers used by PluginController
    // ─────────────────────────────────────────────────────────────────────────

    /**
     * Read the list of active plugin slugs from storage.
     * Returns an empty array on any error.
     *
     * @param string $basePath CMS root path
     * @return string[]
     */
    public static function getActiveSlugs(string $basePath): array
    {
        $activeFile = $basePath . '/' . self::ACTIVE_FILE;

        if (!is_file($activeFile)) {
            return [];
        }

        return self::readActiveFile($activeFile);
    }

    /**
     * Persist the list of active plugin slugs to storage.
     * Slugs are whitelist-validated before saving.
     *
     * @param string   $basePath CMS root path
     * @param string[] $slugs    Array of slug strings to activate
     * @return bool True on success
     */
    public static function saveActiveSlugs(string $basePath, array $slugs): bool
    {
        // Sanitize: only valid slugs survive
        $clean = [];
        foreach ($slugs as $slug) {
            if (is_string($slug) && preg_match(self::SLUG_PATTERN, $slug)) {
                $clean[] = $slug;
            }
        }

        // Deduplicate and re-index
        $clean = array_values(array_unique($clean));

        $activeFile = $basePath . '/' . self::ACTIVE_FILE;
        $storageDir = dirname($activeFile);

        if (!is_dir($storageDir)) {
            return false;
        }

        $json = json_encode($clean, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
        if ($json === false) {
            return false;
        }

        return file_put_contents($activeFile, $json) !== false;
    }

    // ─────────────────────────────────────────────────────────────────────────
    // Private helpers
    // ─────────────────────────────────────────────────────────────────────────

    /**
     * Load a single plugin's plugin.php with full security checks.
     * A failed load is logged but does not throw.
     */
    private static function loadPlugin(string $pluginsBase, mixed $slug): void
    {
        // ── 1. Slug must be a non-empty string passing the whitelist ──────────
        if (!is_string($slug) || !preg_match(self::SLUG_PATTERN, $slug)) {
            error_log('[PluginLoader] Skipping plugin with invalid slug: ' . print_r($slug, true));
            return;
        }

        // ── 2. Plugin directory must exist ────────────────────────────────────
        $pluginDir = $pluginsBase . '/' . $slug;
        if (!is_dir($pluginDir)) {
            error_log('[PluginLoader] Plugin directory not found, skipping: ' . $slug);
            return;
        }

        // ── 3. Entry-point file must exist ────────────────────────────────────
        $entryPoint = $pluginDir . '/plugin.php';
        if (!is_file($entryPoint)) {
            error_log('[PluginLoader] plugin.php not found for plugin: ' . $slug);
            return;
        }

        // ── 4. Realpath check – entry-point must be inside plugins/{slug}/ ────
        $realPluginDir  = realpath($pluginDir);
        $realEntryPoint = realpath($entryPoint);

        if ($realPluginDir === false || $realEntryPoint === false) {
            error_log('[PluginLoader] Realpath resolution failed for plugin: ' . $slug);
            return;
        }

        // Ensure plugin.php is genuinely inside the plugin directory
        $prefix = $realPluginDir . DIRECTORY_SEPARATOR;
        if (strpos($realEntryPoint, $prefix) !== 0) {
            error_log('[PluginLoader] Security: plugin.php outside plugin directory for: ' . $slug);
            return;
        }

        // ── 5. Load with error isolation ──────────────────────────────────────
        try {
            (static function (string $__pluginFile): void {
                include $__pluginFile;
            })($entryPoint);
        } catch (\Throwable $e) {
            error_log('[PluginLoader] Error loading plugin "' . $slug . '": ' . $e->getMessage());
        }
    }

    /**
     * Parse and validate the active-plugins JSON file.
     * Returns a flat array of strings.
     *
     * @return string[]
     */
    private static function readActiveFile(string $path): array
    {
        $raw = @file_get_contents($path);
        if ($raw === false) {
            return [];
        }

        $data = json_decode($raw, true);
        if (!is_array($data)) {
            return [];
        }

        // Return only string values (defensive)
        return array_values(array_filter($data, 'is_string'));
    }
}
