<?php
declare(strict_types=1);

/**
 * Drains the `_bundled-plugins/` staging directory shipped inside every
 * release ZIP into the live `plugins/` directory.
 *
 * Why staging at all? The legacy auto-updater (Plenvo <= 1.14.0) blanket-skips
 * any ZIP path matching `plugins/`, so an official plugin can never reach an
 * existing installation that way. The newer updater (>= 1.14.0) has a
 * whitelist for `plugins/ldap/` and `plugins/impersonate/`, but customers who
 * are still on the old updater never reach the whitelist. Shipping the same
 * files under a path the old updater does not recognise is the bootstrap.
 *
 * Behaviour: idempotent, runs at boot, copies recursively (overwriting any
 * stale files), then removes the staging dir. If `_bundled-plugins/` is not
 * present the call is a cheap no-op.
 */
class BundledPluginInstaller
{
    public static function run(): void
    {
        $staging = BASE_PATH . '/_bundled-plugins';
        if (!is_dir($staging)) return;

        $entries = scandir($staging) ?: [];
        foreach ($entries as $name) {
            if ($name === '.' || $name === '..') continue;
            $sourceDir = $staging . '/' . $name;
            if (!is_dir($sourceDir)) continue;
            // Constrain plugin id shape; the filename came from a release ZIP
            // we built, but defence-in-depth is cheap here.
            if (!preg_match('/^[A-Za-z0-9_-]+$/', $name)) continue;

            $targetDir = BASE_PATH . '/plugins/' . $name;
            try {
                self::copyDir($sourceDir, $targetDir);
            } catch (\Throwable $e) {
                error_log('[Plenvo] BundledPluginInstaller failed for ' . $name . ': ' . $e->getMessage());
                // Leave the staging dir in place so a later boot can retry.
                return;
            }
        }

        // All entries copied successfully. Drop the staging tree.
        try {
            self::removeDir($staging);
        } catch (\Throwable $e) {
            error_log('[Plenvo] BundledPluginInstaller cleanup failed: ' . $e->getMessage());
        }
    }

    private static function copyDir(string $src, string $dst): void
    {
        if (!is_dir($dst) && !@mkdir($dst, 0755, true) && !is_dir($dst)) {
            throw new \RuntimeException('mkdir failed: ' . $dst);
        }
        foreach (scandir($src) ?: [] as $entry) {
            if ($entry === '.' || $entry === '..') continue;
            $sf = $src . '/' . $entry;
            $df = $dst . '/' . $entry;
            if (is_dir($sf)) {
                self::copyDir($sf, $df);
            } else {
                if (@copy($sf, $df) === false) {
                    throw new \RuntimeException('copy failed: ' . $sf . ' -> ' . $df);
                }
            }
        }
    }

    private static function removeDir(string $dir): void
    {
        if (!is_dir($dir)) return;
        foreach (scandir($dir) ?: [] as $entry) {
            if ($entry === '.' || $entry === '..') continue;
            $p = $dir . '/' . $entry;
            if (is_dir($p)) {
                self::removeDir($p);
            } else {
                @unlink($p);
            }
        }
        @rmdir($dir);
    }
}
