<?php

declare(strict_types=1);

namespace StaticPHP\Package;

use StaticPHP\Artifact\Artifact;
use StaticPHP\Artifact\ArtifactCache;
use StaticPHP\Artifact\ArtifactDownloader;
use StaticPHP\Artifact\ArtifactExtractor;
use StaticPHP\Artifact\DownloaderOptions;
use StaticPHP\Config\PackageConfig;
use StaticPHP\DI\ApplicationContext;
use StaticPHP\Exception\WrongUsageException;
use StaticPHP\Registry\PackageLoader;
use StaticPHP\Runtime\SystemTarget;
use StaticPHP\Util\BuildRootTracker;
use StaticPHP\Util\DependencyResolver;
use StaticPHP\Util\DirDiff;
use StaticPHP\Util\FileSystem;
use StaticPHP\Util\GlobalEnvManager;
use StaticPHP\Util\InteractiveTerm;
use StaticPHP\Util\LicenseDumper;
use StaticPHP\Util\V2CompatLayer;
use ZM\Logger\ConsoleColor;

/**
 * PackageInstaller is responsible for installing packages within the StaticPHP framework.
 */
class PackageInstaller
{
    /** @var array<string, Package> Resolved package list */
    protected array $packages = [];

    /** @var array<string, Package> Packages to be built from source */
    protected array $build_packages = [];

    /** @var array<string, Package> Packages to be installed */
    protected array $install_packages = [];

    /** @var array<string, array<string>> Unresolved target additional dependencies defined in #[ResolveBuild] */
    protected array $target_additional_dependencies = [];

    /** @var bool Whether to download missing sources automatically */
    protected bool $download = true;

    /** @var null|BuildRootTracker buildroot file tracker for debugging purpose */
    protected ?BuildRootTracker $tracker = null;

    public function __construct(protected array $options = [], public readonly bool $interactive = true)
    {
        ApplicationContext::set(PackageInstaller::class, $this);
        $builder = new PackageBuilder($options);
        ApplicationContext::set(PackageBuilder::class, $builder);

        // Check for no-download option
        if (!empty($options['no-download'])) {
            $this->download = false;
        }

        // Initialize BuildRootTracker if tracking is enabled (default: enabled unless --no-tracker)
        if (empty($options['no-tracker'])) {
            $this->tracker = new BuildRootTracker();
        }
    }

    /**
     * Add a package to the build list.
     * This means the package will be built from source.
     */
    public function addBuildPackage(LibraryPackage|string|TargetPackage $package): static
    {
        if (is_string($package)) {
            $package = PackageLoader::getPackage($package);
        }
        // special check for php target packages
        if (in_array($package->getName(), ['php', 'php-cli', 'php-fpm', 'php-micro', 'php-cgi', 'php-embed', 'frankenphp'], true)) {
            if (!$package instanceof TargetPackage) {
                throw new WrongUsageException("Package '{$package->getName()}' is expected to be a TargetPackage.");
            }
            $this->handlePhpTargetPackage($package);
            return $this;
        }
        if (!$package->hasStage('build')) {
            throw new WrongUsageException("Target package '{$package->getName()}' does not define build process for current OS: " . PHP_OS_FAMILY . '.');
        }
        if (($this->options['pack-mode'] ?? false) === true && !empty($this->build_packages)) {
            throw new WrongUsageException("In 'pack-mode', only one package can be built at a time. Cannot add package '{$package->getName()}' to build list.");
        }
        $this->build_packages[$package->getName()] = $package;
        return $this;
    }

    /**
     * @param  string       $name Package name
     * @return null|Package The build package instance or null if not found
     */
    public function getBuildPackage(string $name): ?Package
    {
        return $this->build_packages[$name] ?? null;
    }

    /**
     * Add a package to the installation list.
     * This means the package will try to install binary artifacts first.
     * If no artifacts found, it will fallback to build from source.
     */
    public function addInstallPackage(LibraryPackage|string $package): static
    {
        if (is_string($package)) {
            $package = PackageLoader::getPackage($package);
        }
        $this->install_packages[$package->getName()] = $package;
        return $this;
    }

    /**
     * Set whether to download packages before installation.
     */
    public function setDownload(bool $download = true): static
    {
        $this->download = $download;
        return $this;
    }

    /**
     * Get the BuildRootTracker instance.
     *
     * @return null|BuildRootTracker The tracker instance or null if tracking is disabled
     */
    public function getTracker(): ?BuildRootTracker
    {
        return $this->tracker;
    }

    public function printBuildPackageOutputs(): void
    {
        /** @var Package $package */
        foreach ($this->getResolvedPackages() as $package) {
            if (($outputs = $package->getOutputs()) !== []) {
                InteractiveTerm::notice('Package ' . ConsoleColor::green($package->getName()) . ' outputs');
                $this->printArrayInfo(info: $outputs);
            }
        }
    }

    /**
     * Run the package installation process.
     */
    public function run(bool $disable_delay_msg = false): void
    {
        // apply build toolchain envs
        GlobalEnvManager::afterInit();

        if (empty($this->packages)) {
            // resolve input, make dependency graph
            $this->resolvePackages();
        }

        if ($this->interactive && !$disable_delay_msg) {
            // show install or build options in terminal with beautiful output
            $this->printInstallerInfo();
            InteractiveTerm::notice('Build process will start after 2s ...' . PHP_EOL);
            sleep(2);
        }

        // Early validation: check if packages can be built or installed before downloading
        $this->validatePackagesBeforeBuild();

        // check download
        if ($this->download) {
            $downloaderOptions = DownloaderOptions::extractFromConsoleOptions($this->options, 'dl');
            // Collect packages that have no build stage for current OS but do have a platform binary.
            // These must always download binary (not source), regardless of global prefer-source setting.
            $binary_only_packages = array_filter(
                $this->packages,
                fn ($p) => $p instanceof LibraryPackage
                    && !$this->isBuildPackage($p)
                    && !$p->hasStage('build')
                    && ($p->getArtifact()?->hasPlatformBinary() ?? false)
            );
            $dl_opts = [
                ...$downloaderOptions,
                'source-only' => implode(',', array_map(fn ($x) => $x->getName(), $this->build_packages)),
            ];
            if ($binary_only_packages !== []) {
                $dl_opts['binary-only'] = implode(',', array_map(fn ($x) => $x->getName(), $binary_only_packages));
            }
            $downloader = new ArtifactDownloader($dl_opts, $this->interactive);
            $downloader->addArtifacts($this->getArtifacts())->download();
        } else {
            logger()->notice('Skipping download (--no-download option enabled)');
        }

        // extract sources
        $this->extractSourceArtifacts();

        // validate packages
        foreach ($this->packages as $package) {
            // 1. call validate package
            $package->validatePackage();
        }

        // build/install packages
        if ($this->interactive) {
            InteractiveTerm::notice('Building/Installing packages ...');
            keyboard_interrupt_register(function () {
                InteractiveTerm::finish('Build/Install process interrupted by user!', false);
                exit(130);
            });
        }
        $builder = ApplicationContext::get(PackageBuilder::class);
        foreach ($this->packages as $package) {
            $is_to_build = $this->isBuildPackage($package);
            $has_build_stage = $package instanceof LibraryPackage && $package->hasStage('build');
            $should_use_binary = $package instanceof LibraryPackage && ($package->getArtifact()?->shouldUseBinary() ?? false);
            $has_source = $package->hasSource();
            if (!$is_to_build && $should_use_binary) {
                // install binary
                if ($this->interactive) {
                    InteractiveTerm::indicateProgress('Installing package: ' . ConsoleColor::yellow($package->getName()));
                }
                try {
                    // Start tracking for binary installation
                    $this->tracker?->startTracking($package, 'install');
                    $status = $this->installBinary($package);
                    // Stop tracking and record changes
                    $this->tracker?->stopTracking();
                } catch (\Throwable $e) {
                    // Stop tracking on error
                    $this->tracker?->stopTracking();
                    if ($this->interactive) {
                        InteractiveTerm::finish('Installing binary package failed: ' . ConsoleColor::red($package->getName()), false);
                        echo PHP_EOL;
                    }
                    throw $e;
                }
                if ($this->interactive) {
                    InteractiveTerm::finish('Installed binary package: ' . ConsoleColor::green($package->getName()) . ($status === SPC_STATUS_ALREADY_INSTALLED ? ' (already installed, skipped)' : ''));
                }
            } elseif ($is_to_build && $has_build_stage || $has_source && $has_build_stage) {
                if ($this->interactive) {
                    InteractiveTerm::indicateProgress('Building package: ' . ConsoleColor::yellow($package->getName()));
                }
                try {
                    // Start tracking for build
                    $this->tracker?->startTracking($package, 'build');

                    if ($is_to_build && ($this->options['pack-mode'] ?? false) === true) {
                        $dirdiff = new DirDiff(BUILD_ROOT_PATH, false);
                        ApplicationContext::set(DirDiff::class, $dirdiff);
                    }
                    /** @var LibraryPackage $package */
                    $status = $builder->buildPackage($package, $this->isBuildPackage($package));

                    if ($is_to_build && ($this->options['pack-mode'] ?? false) === true) {
                        $package->runStage('packPrebuilt');
                    }

                    // Stop tracking and record changes
                    $this->tracker?->stopTracking();
                } catch (\Throwable $e) {
                    // Stop tracking on error
                    $this->tracker?->stopTracking();
                    if ($this->interactive) {
                        InteractiveTerm::finish('Building package failed: ' . ConsoleColor::red($package->getName()), false);
                        echo PHP_EOL;
                    }
                    throw $e;
                }
                if ($this->interactive) {
                    InteractiveTerm::finish('Built package: ' . ConsoleColor::green($package->getName()) . ($status === SPC_STATUS_ALREADY_BUILT ? ' (already built, skipped)' : ''));
                }
            }
        }

        // perform after-install actions and emit post-install events
        $this->emitPostInstallEvents();
    }

    public function isBuildPackage(Package|string $package): bool
    {
        return isset($this->build_packages[is_string($package) ? $package : $package->getName()]);
    }

    /**
     * Get all resolved packages.
     * You can filter by package type class if needed.
     *
     * @template T
     * @param  class-string<T> $package_type Filter by package type
     * @return array<T>
     */
    public function getResolvedPackages(mixed $package_type = Package::class): array
    {
        return array_filter($this->packages, function (Package $pkg) use ($package_type): bool {
            return $pkg instanceof $package_type;
        });
    }

    public function isPackageResolved(string $package_name): bool
    {
        return isset($this->packages[$package_name]);
    }

    /**
     * Get resolved package names filtered to only packages whose build artifacts are available.
     * This excludes library packages that haven't been built/installed yet, which naturally
     * prevents SPCConfigUtil from checking static-lib files of libraries that come after
     * the current target in the build order (e.g. 'watcher' for frankenphp isn't built
     * when 'php' is being compiled).
     *
     * @return string[] Available resolved package names
     */
    public function getAvailableResolvedPackageNames(): array
    {
        return array_values(array_filter(
            array_keys($this->packages),
            function (string $name): bool {
                $pkg = $this->packages[$name] ?? null;
                // Exclude library packages whose build artifacts don't exist yet.
                // Extensions and targets are not filtered — extensions are compiled into PHP
                // and don't have standalone build artifacts.
                if ($pkg instanceof LibraryPackage && $pkg->getType() === 'library' && !$pkg->isInstalled()) {
                    return false;
                }
                return true;
            }
        ));
    }

    public function isPackageInstalled(Package|string $package_name): bool
    {
        if (empty($this->packages)) {
            $this->resolvePackages();
        }
        if (is_string($package_name)) {
            $package = $this->getPackage($package_name);
            if ($package === null) {
                throw new WrongUsageException("Package '{$package_name}' is not resolved.");
            }
        } else {
            $package = $package_name;
        }

        // check if package is built/installed
        if ($this->isBuildPackage($package)) {
            return $package->isInstalled();
        }
        if ($package->getArtifact() !== null && $package->getArtifact()->shouldUseBinary()) {
            $artifact = $package->getArtifact();
            return $artifact->isBinaryExtracted();
        }
        // Fallback: if the download cache is missing (e.g. download failed or cache was cleared),
        // still check whether the files are physically present in buildroot.
        // Note: TargetPackage extends LibraryPackage, but target packages (e.g. zig) have no
        // static-libs/headers configured, so isInstalled() would trivially return true for them.
        // Only apply this fallback to pure library packages.
        if ($package instanceof LibraryPackage && !($package instanceof TargetPackage)) {
            return $package->isInstalled();
        }
        return false;
    }

    public function emitPostInstallEvents(): void
    {
        foreach ($this->packages as $package) {
            if ($package->hasStage('postInstall')) {
                $package->runStage('postInstall');
            }
        }

        $this->dumpLicenseFiles($this->packages);
    }

    /**
     * Returns the download status of all artifacts for the resolved packages.
     *
     * @return array<string, array{
     *     source-downloaded: bool,
     *     binary-downloaded: bool,
     *     has-source: bool,
     *     has-binary: bool
     * }> artifact name => [source downloaded, binary downloaded]
     */
    public function getArtifactDownloadStatus(): array
    {
        $download_status = [];
        foreach ($this->getResolvedPackages() as $package) {
            if (($artifact = $package->getArtifact()) !== null && !isset($download_status[$artifact->getName()])) {
                // [0: source, 1: binary for current OS]
                $download_status[$artifact->getName()] = [
                    'source-downloaded' => $artifact->isSourceDownloaded(),
                    'binary-downloaded' => $artifact->isBinaryDownloaded(),
                    'has-source' => $artifact->hasSource(),
                    'has-binary' => $artifact->hasPlatformBinary(),
                ];
                $download_status[$artifact->getName()] = [$artifact->isSourceDownloaded(), $artifact->isBinaryDownloaded()];
            }
        }
        return $download_status;
    }

    /**
     * Get all artifacts from resolved and build packages.
     *
     * @return Artifact[]
     */
    public function getArtifacts(): array
    {
        $artifacts = [];
        foreach ($this->getResolvedPackages() as $package) {
            // Validate package artifacts
            $this->validatePackageArtifact($package);
            if (($artifact = $package->getArtifact()) !== null && !in_array($artifact, $artifacts, true)) {
                $artifacts[] = $artifact;
            }
        }
        // add target artifacts
        foreach ($this->build_packages as $package) {
            // Validate package artifacts
            $this->validatePackageArtifact($package);
            if (($artifact = $package->getArtifact()) !== null && !in_array($artifact, $artifacts, true)) {
                $artifacts[] = $artifact;
            }
        }
        return $artifacts;
    }

    /**
     * Extract all artifacts for resolved packages.
     */
    public function extractSourceArtifacts(): void
    {
        FileSystem::createDir(SOURCE_PATH);
        $packages = array_values($this->packages);

        $cache = ApplicationContext::get(ArtifactCache::class);
        $extractor = new ArtifactExtractor($cache);

        // Collect all unique artifacts
        $artifacts = [];
        $pkg_artifact_map = [];
        foreach ($packages as $package) {
            $artifact = $package->getArtifact();
            if ($artifact !== null && !isset($artifacts[$artifact->getName()]) && (!$artifact->shouldUseBinary() || $this->isBuildPackage($package))) {
                $pkg_artifact_map[$package->getName()] = $artifact->getName();
                $artifacts[$artifact->getName()] = $artifact;
            }
        }

        // Sort: php-src should be extracted first (extensions depend on it)
        uksort($artifacts, function (string $a, string $b): int {
            if ($a === 'php-src') {
                return -1;
            }
            if ($b === 'php-src') {
                return 1;
            }
            return 0;
        });

        if (count($artifacts) === 0) {
            return;
        }

        // Extract each artifact
        if ($this->interactive) {
            InteractiveTerm::notice('Extracting source for ' . count($artifacts) . ' artifacts: ' . implode(',', array_map(fn ($x) => ConsoleColor::yellow($x->getName()), $artifacts)) . ' ...');
            InteractiveTerm::indicateProgress('Extracting artifacts');
        }

        try {
            V2CompatLayer::beforeExtsExtractHook();
            foreach ($artifacts as $artifact) {
                if ($this->interactive) {
                    InteractiveTerm::setMessage('Extracting source: ' . ConsoleColor::green($artifact->getName()));
                }
                if (($pkg = array_search($artifact->getName(), $pkg_artifact_map, true)) !== false) {
                    V2CompatLayer::beforeLibExtractHook($pkg);
                }
                $extractor->extract($artifact, true);
                if (($pkg = array_search($artifact->getName(), $pkg_artifact_map, true)) !== false) {
                    V2CompatLayer::afterLibExtractHook($pkg);
                }
            }
            V2CompatLayer::afterExtsExtractHook();
            if ($this->interactive) {
                InteractiveTerm::finish('Extracted all sources successfully.');
                echo PHP_EOL;
            }
        } catch (\Throwable $e) {
            if ($this->interactive) {
                InteractiveTerm::finish('Artifact extraction failed!', false);
                echo PHP_EOL;
            }
            throw $e;
        }
    }

    public function installBinary(Package $package): int
    {
        $extractor = new ArtifactExtractor(ApplicationContext::get(ArtifactCache::class));
        $artifact = $package->getArtifact();
        if ($artifact === null || !$artifact->shouldUseBinary()) {
            throw new WrongUsageException("Package '{$package->getName()}' does not have a binary artifact to install.");
        }

        $status = $extractor->extract($artifact);
        if ($status === SPC_STATUS_ALREADY_EXTRACTED) {
            return SPC_STATUS_ALREADY_INSTALLED;
        }

        // perform package after-install actions
        $this->performAfterInstallActions($package);
        return SPC_STATUS_INSTALLED;
    }

    /**
     * @internal internally calling only, for users, please use specific getter, such as 'getLibraryPackage', 'getTaretPackage', etc
     * @param string $package_name Package name
     */
    public function getPackage(string $package_name): ?Package
    {
        return $this->packages[$package_name] ?? null;
    }

    /**
     * Get a library package by name.
     *
     * @param  string              $package_name Package name
     * @return null|LibraryPackage The library package instance or null if not found
     */
    public function getLibraryPackage(string $package_name): ?LibraryPackage
    {
        $pkg = $this->getPackage($package_name);
        if ($pkg instanceof LibraryPackage) {
            return $pkg;
        }
        return null;
    }

    /**
     * Get a target package by name.
     *
     * @param  string             $package_name Package name
     * @return null|TargetPackage The target package instance or null if not found
     */
    public function getTargetPackage(string $package_name): ?TargetPackage
    {
        $pkg = $this->getPackage($package_name);
        if ($pkg instanceof TargetPackage) {
            return $pkg;
        }
        return null;
    }

    /**
     * Get a PHP extension by name.
     *
     * @param  string                   $package_or_ext_name Extension name
     * @return null|PhpExtensionPackage The target package instance or null if not found
     */
    public function getPhpExtensionPackage(string $package_or_ext_name): ?PhpExtensionPackage
    {
        $pkg = $this->getPackage($package_or_ext_name);
        if ($pkg instanceof PhpExtensionPackage) {
            return $pkg;
        }
        $pkg = $this->getPackage("ext-{$package_or_ext_name}");
        if ($pkg instanceof PhpExtensionPackage) {
            return $pkg;
        }
        return null;
    }

    /**
     * @param Package[] $packages
     */
    private function dumpLicenseFiles(array $packages): void
    {
        $dumper = new LicenseDumper();
        foreach ($packages as $package) {
            $artifact = $package->getArtifact();
            if ($artifact !== null) {
                $dumper->addArtifacts([$artifact->getName()]);
            }
        }
        $dumper->dump(BUILD_ROOT_PATH . '/license');
        if ($this->interactive) {
            InteractiveTerm::success('Exported package licenses', true);
        }
    }

    /**
     * Validate that a package has required artifacts.
     */
    private function validatePackageArtifact(Package $package): void
    {
        // target and library must have at least source or platform binary
        if (in_array($package->getType(), ['library', 'target']) && !$package->getArtifact()?->hasSource() && !$package->getArtifact()?->hasPlatformBinary()) {
            throw new WrongUsageException("Validation failed: Target package '{$package->getName()}' has no source or current platform (" . SystemTarget::getCurrentPlatformString() . ') binary defined.');
        }
    }

    private function resolvePackages(): void
    {
        $pkgs = [];

        foreach ($this->build_packages as $package) {
            // call #[ResolveBuild] annotation methods if defined
            if ($package instanceof TargetPackage && is_array($deps = $package->_emitResolveBuild($this))) {
                $this->target_additional_dependencies[$package->getName()] = $deps;
            }
            $pkgs[] = $package->getName();
        }

        // gather install packages
        foreach ($this->install_packages as $package) {
            $pkgs[] = $package->getName();
        }

        // resolve dependencies string
        $resolved_packages = DependencyResolver::resolve(
            $pkgs,
            $this->target_additional_dependencies,
            $this->options['with-suggests'] ?? false
        );

        foreach ($resolved_packages as $pkg_name) {
            $this->packages[$pkg_name] = PackageLoader::getPackage($pkg_name);
        }

        foreach ($this->packages as $package) {
            $this->injectPackageEnvs($package);
        }
    }

    private function injectPackageEnvs(Package $package): void
    {
        $name = $package->getName();

        $paths = PackageConfig::get($name, 'path', []);
        foreach ($paths as $path) {
            GlobalEnvManager::addPathIfNotExists(FileSystem::replacePathVariable($path));
        }

        $envs = PackageConfig::get($name, 'env', []);
        foreach ($envs as $k => $v) {
            GlobalEnvManager::putenv("{$k}=" . FileSystem::replacePathVariable((string) $v));
        }

        $append_envs = PackageConfig::get($name, 'append-env', []);
        foreach ($append_envs as $k => $v) {
            GlobalEnvManager::appendEnv($k, FileSystem::replacePathVariable((string) $v));
        }
    }

    private function handlePhpTargetPackage(TargetPackage $package): void
    {
        // process 'php' target
        if ($package->getName() === 'php') {
            // logger()->warning("Building 'php' target is deprecated, please use specific targets like 'build:php-cli' instead.");
            $added = false;

            if ($package->getBuildOption('build-all') || $package->getBuildOption('build-cli')) {
                $cli = PackageLoader::getPackage('php-cli');
                $this->install_packages[$cli->getName()] = $cli;
                $added = true;
            }
            if ($package->getBuildOption('build-all') || $package->getBuildOption('build-fpm')) {
                $fpm = PackageLoader::getPackage('php-fpm');
                $this->install_packages[$fpm->getName()] = $fpm;
                $added = true;
            }
            if ($package->getBuildOption('build-all') || $package->getBuildOption('build-micro')) {
                $micro = PackageLoader::getPackage('php-micro');
                $this->install_packages[$micro->getName()] = $micro;
                $added = true;
            }
            if ($package->getBuildOption('build-all') || $package->getBuildOption('build-cgi')) {
                $cgi = PackageLoader::getPackage('php-cgi');
                $this->install_packages[$cgi->getName()] = $cgi;
                $added = true;
            }
            if ($package->getBuildOption('build-all') || $package->getBuildOption('build-embed')) {
                $embed = PackageLoader::getPackage('php-embed');
                $this->install_packages[$embed->getName()] = $embed;
                $added = true;
            }
            if ($package->getBuildOption('build-all') || $package->getBuildOption('build-frankenphp')) {
                $frankenphp = PackageLoader::getPackage('frankenphp');
                $this->install_packages[$frankenphp->getName()] = $frankenphp;
                $this->build_packages[$package->getName()] = $package;
                $added = true;
            }
            $this->build_packages[$package->getName()] = $package;

            if (!$added) {
                throw new WrongUsageException(
                    "No SAPI target specified to build. Please use '--build-cli', '--build-fpm', '--build-micro', " .
                    "'--build-cgi', '--build-embed', '--build-frankenphp' or '--build-all' options."
                );
            }
        } else {
            // process specific php sapi targets
            if ($package->getName() === 'frankenphp') {
                $this->build_packages['php'] = PackageLoader::getPackage('php');
                $this->build_packages['frankenphp'] = PackageLoader::getPackage('frankenphp');
            } else {
                $this->install_packages[$package->getName()] = $package;
                $this->build_packages['php'] = PackageLoader::getPackage('php');
            }
        }
    }

    private function printInstallerInfo(): void
    {
        InteractiveTerm::notice('Installation summary:');
        $summary['Packages to be built'] = implode(',', array_map(fn ($x) => $x->getName(), array_values($this->build_packages)));
        $summary['Packages to be installed'] = implode(',', array_map(fn ($x) => $x->getName(), array_values($this->packages)));
        if (!($this->options['no-download'] ?? false)) {
            $summary['Artifacts to be downloaded'] = implode(',', array_map(fn ($x) => $x->getName(), $this->getArtifacts()));
        } else {
            $summary['Artifacts to be downloaded'] = 'none (--no-download option enabled)';
        }
        $this->printArrayInfo(array_filter($summary));
        echo PHP_EOL;

        foreach ($this->build_packages as $package) {
            $info = $package->getPackageInfo();
            if ($info === []) {
                continue;
            }
            InteractiveTerm::notice("{$package->getName()} build options:");
            // calculate space count for every line
            $this->printArrayInfo($info);
            echo PHP_EOL;
        }
    }

    private function printArrayInfo(array $info): void
    {
        $maxlen = 0;
        foreach ($info as $k => $v) {
            $maxlen = max(strlen($k), $maxlen);
        }
        foreach ($info as $k => $v) {
            if (is_string($v)) {
                InteractiveTerm::plain("  {$k}: " . str_pad('', $maxlen - strlen($k)) . ConsoleColor::yellow($v));
            } elseif (is_array($v) && !is_assoc_array($v)) {
                $first = array_shift($v);
                InteractiveTerm::plain("  {$k}: " . str_pad('', $maxlen - strlen($k)) . ConsoleColor::yellow($first));
                foreach ($v as $vs) {
                    InteractiveTerm::plain(str_pad('', $maxlen + 4) . ConsoleColor::yellow($vs));
                }
            }
        }
    }

    private function validatePackagesBeforeBuild(): void
    {
        foreach ($this->packages as $package) {
            // Check OS support for php-extension packages
            if ($package instanceof PhpExtensionPackage && !$package->isSupportedOnCurrentOS()) {
                $supported = implode(', ', $package->getSupportedOSList());
                throw new WrongUsageException(
                    "Extension '{$package->getName()}' is not supported on current OS: " . SystemTarget::getTargetOS() .
                    ". Supported OS: [{$supported}]"
                );
            }
            if ($package->getType() !== 'library') {
                continue;
            }
            $is_to_build = $this->isBuildPackage($package);
            $has_build_stage = $package instanceof LibraryPackage && $package->hasStage('build');
            // Use hasPlatformBinary() here (not shouldUseBinary()) because this runs before download,
            // so the binary is not yet on disk. We only need to know if a binary is declared for
            // the current platform in the artifact config.
            $has_platform_binary = $package instanceof LibraryPackage && ($package->getArtifact()?->hasPlatformBinary() ?? false);

            // Check if package can neither be built nor installed
            if (!$is_to_build && !$has_platform_binary && !$has_build_stage) {
                throw new WrongUsageException("Package '{$package->getName()}' cannot be installed: no build stage defined and no binary artifact available for current OS: " . SystemTarget::getCurrentPlatformString());
            }
        }
    }

    private function performAfterInstallActions(Package $package): void
    {
        // ----------- perform post-install actions from extracted .package.{pkg_name}.postinstall.json -----------
        $root_dir = ($package->getArtifact()?->getBinaryDir() ?? '') !== '' ? $package->getArtifact()?->getBinaryDir() : null;
        if ($root_dir !== null) {
            $action_json = "{$root_dir}/.package.{$package->getName()}.postinstall.json";
            if (is_file($action_json)) {
                $action_json = json_decode(file_get_contents($action_json), true);
                if (!is_array($action_json)) {
                    throw new WrongUsageException("Invalid post-install action JSON format for package '{$package->getName()}'.");
                }
                $placeholders = get_pack_replace();
                foreach ($action_json as $action) {
                    $action_name = $action['action'] ?? '';
                    switch ($action_name) {
                        // replace-path: => files: [relative_path1, relative_path2]
                        case 'replace-path':
                            $files = $action['files'] ?? [];
                            foreach ($files as $file) {
                                $filepath = $root_dir . "/{$file}";
                                FileSystem::replaceFileStr($filepath, array_values($placeholders), array_keys($placeholders));
                            }
                            break;
                            // replace-to-env: => file: "relative_path", search: "SEARCH_STR", replace-env: "ENV_VAR_NAME"
                        case 'replace-to-env':
                            $file = $action['file'] ?? '';
                            $search = $action['search'] ?? '';
                            $env_var = $action['replace-env'] ?? '';
                            $replace = getenv($env_var) ?: '';
                            $filepath = $root_dir . "/{$file}";
                            FileSystem::replaceFileStr($filepath, $search, $replace);
                            break;
                        default:
                            throw new WrongUsageException("Unknown post-install action '{$action_name}' for package '{$package->getName()}'.");
                    }
                }
                // remove the action file after processing
                unlink($root_dir . "/.package.{$package->getName()}.postinstall.json");
            }
        }
    }
}
