<?php

namespace PluginsNameSpaces\Dev;

use Composer\Script\Event;
use RecursiveIteratorIterator;
use RecursiveDirectoryIterator;

/**
 * Namespace scoping script.
 *
 * The wpsuitepress/framework package ships under the generic
 * "WPSuitePress\" namespace. To prevent fatal class collisions when
 * several plugins built on the same framework are active at once,
 * this script rewrites the bundled copy's namespace to
 * "<PluginNamespace>\Framework\" on every Composer install/update.
 *
 * Both the src/ PHP files AND the framework package's own composer.json
 * autoload prefix are rewritten — the latter so a regenerated autoloader
 * never re-registers the unscoped "WPSuitePress\" PSR-4 mapping.
 *
 * The target namespace is read from the root composer.json:
 *   extra.wpsuitepress.namespace.current
 *
 * Wired via composer.json scripts (post-install-cmd / post-update-cmd).
 */
class ComposerScript
{
    /**
     * The framework's source namespace.
     */
    const SOURCE_NAMESPACE = 'WPSuitePress';

    /**
     * Relative path (from project root) to the framework package root.
     */
    const FRAMEWORK_DIR = 'vendor/wpsuitepress/framework';

    /**
     * Composer entry point.
     *
     * @param Event $event
     * @return void
     */
    public static function run(Event $event)
    {
        try {
            $root      = dirname($event->getComposer()->getConfig()->get('vendor-dir'));
            $namespace = static::resolveNamespace($root);

            if (! $namespace) {
                fwrite(STDERR, "[wpsuitepress] No target namespace configured; skipping scope.\n");
                return;
            }

            $frameworkRoot = $root . '/' . static::FRAMEWORK_DIR;
            $srcDir        = $frameworkRoot . '/src';

            if (! is_dir($srcDir)) {
                fwrite(STDERR, "[wpsuitepress] Framework not found at {$frameworkRoot}; skipping scope.\n");
                return;
            }

            $target = $namespace . '\\Framework';

            // Only src/ carries the namespaced classes that need rewriting.
            $count = static::rewrite($srcDir, $target);

            // Rewrite the framework's autoload prefix (WPSuitePress\ ->
            // <Target>\) in BOTH the package composer.json (keeps it
            // self-describing) and vendor/composer/installed.json (the
            // snapshot Composer's autoload generator actually reads).
            // Without the installed.json rewrite, every `composer
            // dump-autoload` resurrects a stale "WPSuitePress\\" => src/
            // PSR-4 entry, defeating the collision scoping.
            $autoloadChanged  = static::rewriteComposerAutoload($frameworkRoot, $target);
            $autoloadChanged  = static::rewriteInstalledJson($root, $target) || $autoloadChanged;

            // Strip the framework's dev-only files so the shipped copy is
            // lean (src/ + composer.json) regardless of how Composer fetched
            // it (path mirror, source clone or dist archive).
            $pruned = static::prune($frameworkRoot);

            // Regenerate the autoloader so any optimized class-map reflects
            // the rewritten namespaces. Non-fatal if it fails (PSR-4 is
            // path-based and already resolves the scoped classes).
            if ($count > 0 || $autoloadChanged) {
                static::dumpAutoload($root);
            }

            echo "[wpsuitepress] Scoped framework to {$target} ({$count} files, pruned {$pruned} dev paths).\n";
        } catch (\Throwable $e) {
            fwrite(STDERR, '[wpsuitepress] Scoping failed: ' . $e->getMessage() . "\n");
            fwrite(STDERR, "[wpsuitepress] Run \"composer install\" again; the step is idempotent.\n");
        }
    }

    /**
     * Dev-only files/dirs to remove from the bundled framework so the
     * shipped package mirrors a lean Composer dist (src/ + composer.json).
     *
     * @var string[]
     */
    protected static $prunePaths = [
        'vendor',
        'tests',
        '.github',
        '.phpunit.cache',
        '.claude-flow',
        'graphify-out',
        '.git',
        'phpunit.xml',
        'composer.lock',
        '.gitignore',
        '.gitattributes',
        'CHANGELOG.md',
    ];

    /**
     * Remove dev-only files from the bundled framework directory.
     *
     * @param string $frameworkDir
     * @return int Number of paths removed.
     */
    protected static function prune($frameworkDir)
    {
        $removed = 0;

        foreach (static::$prunePaths as $relative) {
            $path = $frameworkDir . '/' . $relative;

            if (is_file($path) || is_link($path)) {
                @unlink($path);
                $removed++;
            } elseif (is_dir($path)) {
                static::deleteTree($path);
                $removed++;
            }
        }

        return $removed;
    }

    /**
     * Recursively delete a directory.
     *
     * @param string $dir
     * @return void
     */
    protected static function deleteTree($dir)
    {
        $items = new \RecursiveIteratorIterator(
            new \RecursiveDirectoryIterator($dir, \RecursiveDirectoryIterator::SKIP_DOTS),
            \RecursiveIteratorIterator::CHILD_FIRST
        );

        foreach ($items as $item) {
            $item->isDir() ? @rmdir($item->getPathname()) : @unlink($item->getPathname());
        }

        @rmdir($dir);
    }

    /**
     * Regenerate Composer's autoloader.
     *
     * @param string $root
     * @return void
     */
    protected static function dumpAutoload($root)
    {
        if (! function_exists('shell_exec')) {
            return;
        }

        $escaped = escapeshellarg($root);
        @shell_exec("cd {$escaped} && composer dump-autoload --no-interaction 2>&1");
    }

    /**
     * Read the desired namespace from the root composer.json.
     *
     * @param string $root
     * @return string|null
     */
    protected static function resolveNamespace($root)
    {
        $file = $root . '/composer.json';

        if (! file_exists($file)) {
            return null;
        }

        $json = json_decode((string) file_get_contents($file), true);

        return $json['extra']['wpsuitepress']['namespace']['current'] ?? null;
    }

    /**
     * Rewrite the framework namespace across all PHP source files.
     *
     * Idempotent: files already scoped contain no SOURCE_NAMESPACE
     * token and are skipped.
     *
     * @param string $dir
     * @param string $target Fully-qualified target namespace.
     * @return int Number of files modified.
     */
    protected static function rewrite($dir, $target)
    {
        if (! is_dir($dir)) {
            return 0;
        }

        $modified = 0;

        $files = new RecursiveIteratorIterator(
            new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS)
        );

        foreach ($files as $file) {
            if ($file->getExtension() !== 'php') {
                continue;
            }

            $contents = (string) file_get_contents($file->getPathname());

            if (strpos($contents, static::SOURCE_NAMESPACE . '\\') === false) {
                continue;
            }

            $updated = str_replace(
                static::SOURCE_NAMESPACE . '\\',
                $target . '\\',
                $contents
            );

            if ($updated !== $contents) {
                file_put_contents($file->getPathname(), $updated);
                $modified++;
            }
        }

        return $modified;
    }

    /**
     * Rewrite the framework package's composer.json PSR-4 autoload prefix
     * from the source namespace to the scoped target.
     *
     * The framework ships `"WPSuitePress\\": "src/"`. Once src/ is scoped,
     * this declaration is the last place the unscoped prefix survives; left
     * untouched, `composer dump-autoload` keeps re-emitting a
     * `WPSuitePress\ => framework/src` PSR-4 mapping in the generated
     * autoloader. Rewriting it to `<Target>\ => src/` keeps the package
     * self-describing and the autoloader free of the unscoped namespace.
     *
     * The `files` autoload entry (helpers.php) is path-based and left as is.
     *
     * Idempotent: a composer.json carrying no source prefix is unchanged.
     *
     * @param string $frameworkRoot
     * @param string $target Fully-qualified target namespace (no trailing slash).
     * @return bool True if the file was modified.
     */
    protected static function rewriteComposerAutoload($frameworkRoot, $target)
    {
        $file = $frameworkRoot . '/composer.json';

        if (! is_file($file)) {
            return false;
        }

        $json = json_decode((string) file_get_contents($file), true);

        if (! is_array($json) || empty($json['autoload']['psr-4'])) {
            return false;
        }

        $changed = false;
        $rebuilt = static::rewritePsr4Map($json['autoload']['psr-4'], $target, $changed);

        if (! $changed) {
            return false;
        }

        $json['autoload']['psr-4'] = $rebuilt;

        file_put_contents(
            $file,
            json_encode($json, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . "\n"
        );

        return true;
    }

    /**
     * Rewrite the framework package's autoload prefix inside
     * vendor/composer/installed.json.
     *
     * installed.json is the metadata snapshot Composer captures at install
     * time and the source `dump-autoload` reads to regenerate
     * autoload_psr4.php / autoload_static.php — NOT each package's live
     * composer.json. Rewriting it here is what actually removes the
     * unscoped "WPSuitePress\" mapping from the generated autoloader.
     *
     * Only the wpsuitepress/framework package entry is touched. Idempotent.
     *
     * @param string $root
     * @param string $target Fully-qualified target namespace (no trailing slash).
     * @return bool True if the file was modified.
     */
    protected static function rewriteInstalledJson($root, $target)
    {
        $file = $root . '/vendor/composer/installed.json';

        if (! is_file($file)) {
            return false;
        }

        $data = json_decode((string) file_get_contents($file), true);

        if (! is_array($data)) {
            return false;
        }

        // Composer 2 wraps packages in a "packages" key; Composer 1 uses a
        // flat array. Operate on a reference so writes land back in $data.
        $packages = isset($data['packages']) && is_array($data['packages'])
            ? $data['packages']
            : $data;

        $package = substr(static::FRAMEWORK_DIR, strlen('vendor/')); // wpsuitepress/framework
        $changed = false;

        foreach ($packages as $i => $entry) {
            if (! is_array($entry) || ($entry['name'] ?? null) !== $package) {
                continue;
            }

            if (empty($entry['autoload']['psr-4'])) {
                continue;
            }

            $entryChanged = false;
            $packages[$i]['autoload']['psr-4'] = static::rewritePsr4Map(
                $entry['autoload']['psr-4'],
                $target,
                $entryChanged
            );

            $changed = $changed || $entryChanged;
        }

        if (! $changed) {
            return false;
        }

        if (isset($data['packages'])) {
            $data['packages'] = $packages;
        } else {
            $data = $packages;
        }

        file_put_contents(
            $file,
            json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . "\n"
        );

        return true;
    }

    /**
     * Rewrite a PSR-4 prefix map, replacing any prefix that begins with the
     * framework's source namespace with the scoped target namespace.
     *
     * @param array $psr4    Original prefix => path(s) map.
     * @param string $target Fully-qualified target namespace (no trailing slash).
     * @param bool   $changed Set to true if any prefix was rewritten.
     * @return array The rewritten map.
     */
    protected static function rewritePsr4Map(array $psr4, $target, &$changed)
    {
        $sourcePrefix = static::SOURCE_NAMESPACE . '\\';
        $rebuilt      = [];

        foreach ($psr4 as $prefix => $path) {
            if (strpos($prefix, $sourcePrefix) === 0) {
                // "WPSuitePress\" (+ any sub-namespace) -> "<Target>\…"
                $remainder = substr($prefix, strlen($sourcePrefix));
                $rebuilt[$target . '\\' . $remainder] = $path;
                $changed   = true;
            } else {
                $rebuilt[$prefix] = $path;
            }
        }

        return $rebuilt;
    }
}
