<?php
declare(strict_types=1);

namespace PpPhp\Core;

class PluginRegistry
{
    private static ?array $registry = null;

    /** @var array<int,array{plugin:string,message:string}> plugins skipped during the last scan */
    private static array $errors = [];

    public static function all(): array
    {
        if (self::$registry === null) {
            self::$registry = self::scan();
        }

        return self::$registry;
    }

    /**
     * Plugins that were skipped during the last scan (a broken, colliding, or
     * incomplete package), so the Plugin Manager can surface them instead of the
     * whole platform failing. Cleared and rebuilt on every scan().
     *
     * @return array<int,array{plugin:string,message:string}>
     */
    public static function errors(): array
    {
        self::all();
        return self::$errors;
    }

    public static function view(): array { return self::all()['view'] ?? []; }
    public static function edit(): array { return self::all()['edit'] ?? []; }
    public static function filter(): array { return self::all()['filter'] ?? []; }
    public static function validation(): array { return self::all()['validation'] ?? []; }
    public static function iconManagers(): array { return self::all()['iconmanager'] ?? []; }

    public static function reset(): void
    {
        self::$registry = null;
    }

    /** Resolve the fully-qualified class actually declared in a Plugin.php file. */
    private static function declaredPluginClass(string $pluginFile): ?string
    {
        $fqcn = self::parseClassName($pluginFile);
        return ($fqcn !== null && class_exists($fqcn)) ? $fqcn : null;
    }

    /**
     * Parse the fully-qualified class name declared in a Plugin.php file WITHOUT
     * executing it. Used to guard against a name collision before require_once
     * (all plugins share the PpPhp\Plugins namespace, and requiring a second file
     * that declares an already-defined class is an uncatchable fatal).
     */
    private static function parseClassName(string $pluginFile): ?string
    {
        $src = @file_get_contents($pluginFile);
        if ($src === false) {
            return null;
        }
        $ns = preg_match('/namespace\s+([^;]+);/', $src, $m) ? trim($m[1]) : 'PpPhp\\Plugins';
        if (preg_match('/\bclass\s+([A-Za-z_][A-Za-z0-9_]*)/', $src, $c)) {
            return ltrim($ns, '\\') . '\\' . $c[1];
        }
        return null;
    }

    private static function scan(): array
    {
        self::$errors = [];
        $registry = [
            'view' => [],
            'edit' => [],
            'filter' => [],
            'validation' => [],
            'iconmanager' => [],
        ];

        $baseDir = constant('BASE_PATH') . '/plugins';

        foreach (['View', 'Edit', 'Filter', 'Validation', 'IconManager'] as $category) {
            $catDir = $baseDir . '/' . $category;

            if (!is_dir($catDir)) {
                continue;
            }

            foreach (scandir($catDir) ?: [] as $entry) {
                if ($entry === '.' || $entry === '..') {
                    continue;
                }

                $pluginDir = $catDir . '/' . $entry;
                $pluginFile = $pluginDir . '/Plugin.php';

                if (!is_dir($pluginDir) || !is_file($pluginFile)) {
                    continue;
                }

                $tag = $category . '/' . basename($pluginDir);

                // Parse the declared class WITHOUT executing the file so we can
                // guard against a name collision — requiring a second file that
                // declares an already-defined class is an uncatchable fatal that
                // would take down the whole platform. One bad plugin never bricks
                // discovery for the rest.
                $parsed = self::parseClassName($pluginFile);
                if ($parsed === null) {
                    self::$errors[] = ['plugin' => $tag, 'message' => 'Plugin.php declares no class.'];
                    continue;
                }

                // Is the class already defined in this process? It can be either a
                // genuine collision (a DIFFERENT plugin declares the same class —
                // requiring this file would be an uncatchable "Cannot redeclare"
                // fatal) OR simply a re-scan where the SAME file is already loaded
                // (classes can't be unloaded, so reset()+all() re-enters here).
                // Compare the declaring file to tell them apart.
                if (class_exists($parsed, false)) {
                    $existingFile = false;
                    try { $existingFile = (new \ReflectionClass($parsed))->getFileName(); } catch (\Throwable) {}
                    if ($existingFile === false || realpath((string) $existingFile) !== realpath($pluginFile)) {
                        self::$errors[] = ['plugin' => $tag, 'message' => "Class {$parsed} is already used by another plugin — rename the class to something unique."];
                        continue;
                    }
                    // Same file, already loaded (re-scan) → skip require, register below.
                } else {
                    // Load defensively: a syntax error (ParseError), a missing
                    // interface/trait/parent (Error), or a fatal at include (Error)
                    // is caught and this plugin is skipped, not the whole scan.
                    try {
                        require_once $pluginFile;
                    } catch (\Throwable $e) {
                        self::$errors[] = ['plugin' => $tag, 'message' => 'Failed to load: ' . $e->getMessage()];
                        continue;
                    }
                }

                // Validation rules always resolve by their DECLARED class name so a
                // folder like "Number" can't accidentally bind to a same-named
                // View/Edit plugin class. Others use the folder name with a
                // declared-class fallback (e.g. "Switch" → class SwitchPlugin).
                if (strtolower($category) === 'validation') {
                    $className = class_exists($parsed) ? $parsed : ('PpPhp\\Plugins\\' . basename($pluginDir));
                } else {
                    $className = 'PpPhp\\Plugins\\' . basename($pluginDir);
                    if (!class_exists($className)) {
                        $className = $parsed;
                    }
                }

                if (!class_exists($className)) {
                    self::$errors[] = ['plugin' => $tag, 'message' => 'Declared class did not load.'];
                    continue;
                }

                // Reading metadata runs the plugin's own static methods — guard it
                // too so a throwing id()/label()/settingsSchema() can't kill the scan.
                try {
                    $reflect = new \ReflectionClass($className);
                    if (!$reflect->isInstantiable() && !$reflect->hasMethod('id')) {
                        self::$errors[] = ['plugin' => $tag, 'message' => 'Class has no id() method.'];
                        continue;
                    }

                    $id = (string) $className::id();
                    if ($id === '') {
                        self::$errors[] = ['plugin' => $tag, 'message' => 'id() returned an empty string.'];
                        continue;
                    }
                    $label = $className::label();
                    $assets = method_exists($className, 'assets') ? $className::assets() : ['js' => [], 'css' => []];
                    $isCore = is_file($pluginDir . '/.core');

                    $item = [
                        'id' => $id,
                        'label' => $label,
                        'path' => $pluginDir,
                        'class' => $className,
                        'assets' => $assets,
                        'core' => $isCore,
                    ];

                    if (method_exists($className, 'settingsSchema')) {
                        $item['settings'] = $className::settingsSchema();
                    }

                    // Optional Plugin-Manager metadata (all guarded — most plugins omit them).
                    $item['version']     = method_exists($className, 'version') ? (string) $className::version() : '';
                    $item['description'] = method_exists($className, 'description') ? (string) $className::description() : '';
                    $item['docsUrl']     = method_exists($className, 'docsUrl') ? (string) $className::docsUrl() : '';
                    $item['vendor']      = method_exists($className, 'vendor') ? (array) $className::vendor() : [];

                    if (strtolower($category) === 'validation') {
                        // Validation plugins describe their parameters via paramsSchema()
                        // and carry extra picker metadata (group / appliesTo / message).
                        $item['params']  = method_exists($className, 'paramsSchema') ? $className::paramsSchema() : [];
                        $item['group']   = method_exists($className, 'group') ? $className::group() : 'General';
                        $item['applies'] = method_exists($className, 'appliesTo') ? $className::appliesTo() : [];
                        $item['message'] = method_exists($className, 'defaultMessage') ? $className::defaultMessage() : '';
                    }

                    if (strtolower($category) === 'iconmanager') {
                        $item['version'] = method_exists($className, 'version') ? $className::version() : '1.0.0';
                        $item['family'] = method_exists($className, 'family') ? $className::family() : 'icon';
                        $item['metadata'] = method_exists($className, 'metadata') ? $className::metadata() : [];
                    }

                    $registry[strtolower($category)][$id] = $item;
                } catch (\Throwable $e) {
                    self::$errors[] = ['plugin' => $tag, 'message' => 'Invalid plugin: ' . $e->getMessage()];
                    continue;
                }
            }
        }

        return $registry;
    }
}
