<?php

declare(strict_types=1);

namespace StaticPHP\Artifact;

use Psr\Log\LogLevel;
use StaticPHP\Artifact\Downloader\DownloadResult;
use StaticPHP\Artifact\Downloader\Type\BitBucketTag;
use StaticPHP\Artifact\Downloader\Type\CheckUpdateInterface;
use StaticPHP\Artifact\Downloader\Type\CheckUpdateResult;
use StaticPHP\Artifact\Downloader\Type\DownloadTypeInterface;
use StaticPHP\Artifact\Downloader\Type\FileList;
use StaticPHP\Artifact\Downloader\Type\Git;
use StaticPHP\Artifact\Downloader\Type\GitHubRelease;
use StaticPHP\Artifact\Downloader\Type\GitHubTarball;
use StaticPHP\Artifact\Downloader\Type\HostedPackageBin;
use StaticPHP\Artifact\Downloader\Type\LocalDir;
use StaticPHP\Artifact\Downloader\Type\PECL;
use StaticPHP\Artifact\Downloader\Type\PhpRelease;
use StaticPHP\Artifact\Downloader\Type\PIE;
use StaticPHP\Artifact\Downloader\Type\Url;
use StaticPHP\Artifact\Downloader\Type\ValidatorInterface;
use StaticPHP\DI\ApplicationContext;
use StaticPHP\Exception\DownloaderException;
use StaticPHP\Exception\ExecutionException;
use StaticPHP\Exception\ValidationException;
use StaticPHP\Exception\WrongUsageException;
use StaticPHP\Registry\ArtifactLoader;
use StaticPHP\Runtime\Shell\Shell;
use StaticPHP\Runtime\SystemTarget;
use StaticPHP\Util\FileSystem;
use StaticPHP\Util\InteractiveTerm;
use Symfony\Component\Console\Output\OutputInterface;
use ZM\Logger\ConsoleColor;

/**
 * Artifact Downloader class
 */
class ArtifactDownloader
{
    public const array DOWNLOADERS = [
        'bitbuckettag' => BitBucketTag::class,
        'filelist' => FileList::class,
        'git' => Git::class,
        'ghrel' => GitHubRelease::class,
        'ghtar' => GitHubTarball::class,
        'ghtagtar' => GitHubTarball::class,
        'local' => LocalDir::class,
        'pie' => PIE::class,
        'pecl' => PECL::class,
        'url' => Url::class,
        'php-release' => PhpRelease::class,
        'hosted' => HostedPackageBin::class,
    ];

    /** @var array<string, class-string<DownloadTypeInterface>> */
    protected array $downloaders = [];

    /** @var array<string, Artifact> Artifact objects */
    protected array $artifacts = [];

    /** @var int Parallel process number (1 and 0 as single-threaded mode) */
    protected int $parallel = 1;

    protected int $retry = 0;

    /** @var array<string, string> Override custom download urls from options */
    protected array $custom_urls = [];

    /** @var array<string, array{0: string, 1: string}> Override custom git options from options ([branch, git url]) */
    protected array $custom_gits = [];

    /** @var array<string, string> Override custom local paths from options */
    protected array $custom_locals = [];

    /** @var int Fetch type preference */
    protected int $default_fetch_pref = Artifact::FETCH_PREFER_SOURCE;

    /** @var array<string, int> Specific fetch preference */
    protected array $fetch_prefs = [];

    /** @var array<string>|bool Whether to ignore cache for specific artifacts or all */
    protected array|bool $ignore_cache = false;

    /** @var bool Whether to enable alternative mirror downloads */
    protected bool $alt = true;

    private array $_before_files;

    /**
     * @param array{
     *     parallel?: int,
     *     retry?: int,
     *     custom-url?: array<string>,
     *     custom-git?: array<string>,
     *     custom-local?: array<string>,
     *     prefer-source?: null|bool|string,
     *     prefer-pre-built?: null|bool|string,
     *     prefer-binary?: null|bool|string,
     *     source-only?: null|bool|string,
     *     binary-only?: null|bool|string,
     *     ignore-cache?: null|bool|string,
     *     ignore-cache-sources?: null|bool|string,
     *     no-alt?: bool,
     *     no-shallow-clone?: bool
     * } $options Downloader options
     */
    public function __construct(protected array $options = [], public readonly bool $interactive = true)
    {
        // Allow setting concurrency via options
        $this->parallel = max(1, (int) ($options['parallel'] ?? 1));
        // Allow setting retry via options
        $this->retry = max(0, (int) ($options['retry'] ?? 0));
        // Prefer source (default)
        if (array_key_exists('prefer-source', $options)) {
            if (is_string($options['prefer-source'])) {
                $ls = parse_comma_list($options['prefer-source']);
                foreach ($ls as $name) {
                    $this->fetch_prefs[$name] = Artifact::FETCH_PREFER_SOURCE;
                }
            } elseif ($options['prefer-source'] === null || $options['prefer-source'] === true) {
                $this->default_fetch_pref = Artifact::FETCH_PREFER_SOURCE;
            }
        }
        // Prefer binary (originally prefer-pre-built)
        if (array_key_exists('prefer-binary', $options)) {
            if (is_string($options['prefer-binary'])) {
                $ls = parse_comma_list($options['prefer-binary']);
                foreach ($ls as $name) {
                    $this->fetch_prefs[$name] = Artifact::FETCH_PREFER_BINARY;
                }
            } elseif ($options['prefer-binary'] === null || $options['prefer-binary'] === true) {
                $this->default_fetch_pref = Artifact::FETCH_PREFER_BINARY;
            }
        }
        if (array_key_exists('prefer-pre-built', $options)) {
            if (is_string($options['prefer-pre-built'])) {
                $ls = parse_comma_list($options['prefer-pre-built']);
                foreach ($ls as $name) {
                    $this->fetch_prefs[$name] = Artifact::FETCH_PREFER_BINARY;
                }
            } elseif ($options['prefer-pre-built'] === null || $options['prefer-pre-built'] === true) {
                $this->default_fetch_pref = Artifact::FETCH_PREFER_BINARY;
            }
        }
        // Source only
        if (array_key_exists('source-only', $options)) {
            if (is_string($options['source-only'])) {
                $ls = parse_comma_list($options['source-only']);
                foreach ($ls as $name) {
                    $this->fetch_prefs[$name] = Artifact::FETCH_ONLY_SOURCE;
                }
            } elseif ($options['source-only'] === null || $options['source-only'] === true) {
                $this->default_fetch_pref = Artifact::FETCH_ONLY_SOURCE;
            }
        }
        // Binary only
        if (array_key_exists('binary-only', $options)) {
            if (is_string($options['binary-only'])) {
                $ls = parse_comma_list($options['binary-only']);
                foreach ($ls as $name) {
                    $this->fetch_prefs[$name] = Artifact::FETCH_ONLY_BINARY;
                }
            } elseif ($options['binary-only'] === null || $options['binary-only'] === true) {
                $this->default_fetch_pref = Artifact::FETCH_ONLY_BINARY;
            }
        }
        // Ignore cache
        if (array_key_exists('ignore-cache', $options)) {
            if (is_string($options['ignore-cache'])) {
                $this->ignore_cache = parse_comma_list($options['ignore-cache']);
            } elseif ($options['ignore-cache'] === null || $options['ignore-cache'] === true) {
                $this->ignore_cache = true;
            }
        }
        // backward compatibility for ignore-cache-sources
        if (array_key_exists('ignore-cache-sources', $options)) {
            if (is_string($options['ignore-cache-sources'])) {
                $this->ignore_cache = parse_comma_list($options['ignore-cache-sources']);
            } elseif ($options['ignore-cache-sources'] === null || $options['ignore-cache-sources'] === true) {
                $this->ignore_cache = true;
            }
        }
        // Allow setting custom urls via options
        foreach (($options['custom-url'] ?? []) as $value) {
            [$artifact_name, $url] = explode(':', $value, 2);
            $this->custom_urls[$artifact_name] = $url;
            $this->ignore_cache = match ($this->ignore_cache) {
                true => true,
                false => [$artifact_name],
                default => array_merge($this->ignore_cache, [$artifact_name]),
            };
        }
        // Allow setting custom git options via options
        foreach (($options['custom-git'] ?? []) as $value) {
            [$artifact_name, $branch, $git_url] = explode(':', $value, 3) + [null, null, null];
            $this->custom_gits[$artifact_name] = [$branch ?? 'main', $git_url];
            $this->ignore_cache = match ($this->ignore_cache) {
                true => true,
                false => [$artifact_name],
                default => array_merge($this->ignore_cache, [$artifact_name]),
            };
        }
        // Allow setting custom local paths via options
        foreach (($options['custom-local'] ?? []) as $value) {
            [$artifact_name, $local_path] = explode(':', $value, 2);
            $this->custom_locals[$artifact_name] = $local_path;
            $this->ignore_cache = match ($this->ignore_cache) {
                true => true,
                false => [$artifact_name],
                default => array_merge($this->ignore_cache, [$artifact_name]),
            };
        }
        // no alt
        if (array_key_exists('no-alt', $options) && $options['no-alt'] === true) {
            $this->alt = false;
        }

        // read downloads dir
        $this->_before_files = FileSystem::scanDirFiles(DOWNLOAD_PATH, false, true, true) ?: [];

        // load downloaders
        $this->downloaders = self::DOWNLOADERS;
    }

    /**
     * Add an artifact to the download list.
     *
     * @param Artifact|string $artifact Artifact instance or artifact name
     */
    public function add(Artifact|string $artifact): static
    {
        if (is_string($artifact)) {
            $artifact_instance = ArtifactLoader::getArtifactInstance($artifact);
        } else {
            $artifact_instance = $artifact;
        }
        if ($artifact_instance === null) {
            $name = $artifact;
            throw new WrongUsageException("Artifact '{$name}' not found, please check the name.");
        }
        // only add if not already added
        if (!isset($this->artifacts[$artifact_instance->getName()])) {
            $this->artifacts[$artifact_instance->getName()] = $artifact_instance;
        }
        return $this;
    }

    /**
     * Add multiple artifacts to the download list.
     *
     * @param array<Artifact|string> $artifacts Multiple artifacts to add
     */
    public function addArtifacts(array $artifacts): static
    {
        foreach ($artifacts as $artifact) {
            $this->add($artifact);
        }
        return $this;
    }

    /**
     * Set the concurrency limit for parallel downloads.
     *
     * @param int $parallel Number of concurrent downloads (default: 3)
     */
    public function setParallel(int $parallel): static
    {
        $this->parallel = max(1, $parallel);
        return $this;
    }

    /**
     * Download all artifacts, with optional parallel processing.
     */
    public function download(): void
    {
        if ($this->interactive) {
            Shell::passthruCallback(function () {
                InteractiveTerm::advance();
            });
            keyboard_interrupt_register(function () {
                echo PHP_EOL;
                InteractiveTerm::error('Download cancelled by user.');
                // scan changed files
                $after_files = FileSystem::scanDirFiles(DOWNLOAD_PATH, false, true, true) ?: [];
                $new_files = array_diff($after_files, $this->_before_files);

                // remove new files
                foreach ($new_files as $file) {
                    if ($file === '.cache.json') {
                        continue;
                    }
                    logger()->debug("Removing corrupted artifact: {$file}");
                    $path = DOWNLOAD_PATH . DIRECTORY_SEPARATOR . $file;
                    if (is_dir($path)) {
                        FileSystem::removeDir($path);
                    } elseif (is_file($path)) {
                        FileSystem::removeFileIfExists($path);
                    }
                }
                exit(130);
            });
        }

        $this->applyCustomDownloads();

        $count = count($this->artifacts);
        $artifacts_str = implode(',', array_map(fn ($x) => '' . ConsoleColor::yellow($x->getName()), $this->artifacts));
        // mute the first line if not interactive
        if ($this->interactive) {
            InteractiveTerm::notice("Downloading {$count} artifacts: {$artifacts_str} ...");
        }
        try {
            // Create dir
            if (!is_dir(DOWNLOAD_PATH)) {
                FileSystem::createDir(DOWNLOAD_PATH);
            }
            logger()->info('Downloading' . implode(', ', array_map(fn ($x) => " '{$x->getName()}'", $this->artifacts)) . " with concurrency {$this->parallel} ...");
            // Download artifacts parallelly
            if ($this->parallel > 1) {
                $this->downloadWithConcurrency();
            } else {
                // normal sequential download
                $current = 0;
                $skipped = [];
                foreach ($this->artifacts as $artifact) {
                    ++$current;
                    if ($this->downloadWithType($artifact, $current, $count) === SPC_DOWNLOAD_STATUS_SKIPPED) {
                        $skipped[] = $artifact->getName();
                        continue;
                    }
                    $this->_before_files = FileSystem::scanDirFiles(DOWNLOAD_PATH, false, true, true) ?: [];
                }
                if ($this->interactive) {
                    $skip_msg = !empty($skipped) ? ' (Skipped ' . count($skipped) . ' artifacts for being already downloaded)' : '';
                    InteractiveTerm::success("Downloaded all {$count} artifacts.{$skip_msg}\n", true);
                }
            }
        } finally {
            if ($this->interactive) {
                Shell::passthruCallback(null);
                keyboard_interrupt_unregister();
            }
        }
    }

    public function checkUpdate(string $artifact_name, bool $prefer_source = false, bool $bare = false): CheckUpdateResult
    {
        $artifact = ArtifactLoader::getArtifactInstance($artifact_name);
        if ($artifact === null) {
            throw new WrongUsageException("Artifact '{$artifact_name}' not found, please check the name.");
        }
        if ($bare) {
            [$first, $second] = $prefer_source
                ? [fn () => $this->probeSourceCheckUpdate($artifact, $artifact_name), fn () => $this->probeBinaryCheckUpdate($artifact, $artifact_name)]
                : [fn () => $this->probeBinaryCheckUpdate($artifact, $artifact_name), fn () => $this->probeSourceCheckUpdate($artifact, $artifact_name)];
            $result = $first() ?? $second();
            if ($result !== null) {
                return $result;
            }
            // logger()->warning("Artifact '{$artifact_name}' downloader does not support update checking, skipping.");
            return new CheckUpdateResult(old: null, new: null, needUpdate: false, unsupported: true);
        }
        $cache = ApplicationContext::get(ArtifactCache::class);
        if ($prefer_source) {
            $info = $cache->getSourceInfo($artifact_name) ?? $cache->getBinaryInfo($artifact_name, SystemTarget::getCurrentPlatformString());
        } else {
            $info = $cache->getBinaryInfo($artifact_name, SystemTarget::getCurrentPlatformString()) ?? $cache->getSourceInfo($artifact_name);
        }
        if ($info === null) {
            throw new WrongUsageException("Artifact '{$artifact_name}' is not downloaded yet, cannot check update.");
        }
        if (is_a($info['downloader'] ?? null, CheckUpdateInterface::class, true)) {
            $cls = $info['downloader'];
            /** @var CheckUpdateInterface $downloader */
            $downloader = new $cls();
            return $downloader->checkUpdate($artifact_name, $info['config'], $info['version'], $this);
        }

        if (($info['lock_type'] ?? null) === 'source' && ($callback = $artifact->getCustomSourceCheckUpdateCallback()) !== null) {
            return ApplicationContext::invoke($callback, [
                ArtifactDownloader::class => $this,
                'old_version' => $info['version'],
            ]);
        }

        if (($callback = $artifact->getCustomBinaryCheckUpdateCallback()) !== null) {
            return ApplicationContext::invoke($callback, [
                ArtifactDownloader::class => $this,
                'old_version' => $info['version'],
            ]);
        }
        // logger()->warning("Artifact '{$artifact_name}' downloader does not support update checking, skipping.");
        return new CheckUpdateResult(old: null, new: null, needUpdate: false, unsupported: true);
    }

    /**
     * Check updates for multiple artifacts, with optional parallel processing.
     *
     * @param  array<string>                    $artifact_names Artifact names to check
     * @param  bool                             $prefer_source  Whether to prefer source over binary
     * @param  bool                             $bare           Check without requiring artifact to be downloaded first
     * @param  null|callable                    $onResult       Called immediately with (string $name, CheckUpdateResult) as each result arrives
     * @return array<string, CheckUpdateResult> Results keyed by artifact name
     */
    public function checkUpdates(array $artifact_names, bool $prefer_source = false, bool $bare = false, ?callable $onResult = null): array
    {
        if ($this->parallel > 1 && count($artifact_names) > 1) {
            return $this->checkUpdatesWithConcurrency($artifact_names, $prefer_source, $bare, $onResult);
        }
        $results = [];
        foreach ($artifact_names as $name) {
            $result = $this->checkUpdate($name, $prefer_source, $bare);
            $results[$name] = $result;
            if ($onResult !== null) {
                ($onResult)($name, $result);
            }
        }
        return $results;
    }

    public function getRetry(): int
    {
        return $this->retry;
    }

    public function getArtifacts(): array
    {
        return $this->artifacts;
    }

    public function getOption(string $name, mixed $default = null): mixed
    {
        return $this->options[$name] ?? $default;
    }

    private function checkUpdatesWithConcurrency(array $artifact_names, bool $prefer_source, bool $bare, ?callable $onResult): array
    {
        $results = [];
        $fiber_pool = [];
        $remaining = $artifact_names;

        Shell::passthruCallback(function () {
            \Fiber::suspend();
        });

        try {
            while (!empty($remaining) || !empty($fiber_pool)) {
                // fill pool
                while (count($fiber_pool) < $this->parallel && !empty($remaining)) {
                    $name = array_shift($remaining);
                    $fiber = new \Fiber(function () use ($name, $prefer_source, $bare) {
                        return [$name, $this->checkUpdate($name, $prefer_source, $bare)];
                    });
                    $fiber->start();
                    $fiber_pool[$name] = $fiber;
                }
                // check pool
                foreach ($fiber_pool as $fiber_name => $fiber) {
                    if ($fiber->isTerminated()) {
                        // getReturn() re-throws if the fiber threw — propagates immediately
                        [$artifact_name, $result] = $fiber->getReturn();
                        $results[$artifact_name] = $result;
                        if ($onResult !== null) {
                            ($onResult)($artifact_name, $result);
                        }
                        unset($fiber_pool[$fiber_name]);
                    } else {
                        $fiber->resume();
                    }
                }
            }
        } catch (\Throwable $e) {
            // terminate all still-suspended fibers so their curl processes don't hang
            foreach ($fiber_pool as $fiber) {
                if (!$fiber->isTerminated()) {
                    try {
                        $fiber->throw($e);
                    } catch (\Throwable) {
                        // ignore — we only care about stopping them
                    }
                }
            }
            throw $e;
        } finally {
            Shell::passthruCallback(null);
        }

        return $results;
    }

    private function probeSourceCheckUpdate(Artifact $artifact, string $artifact_name): ?CheckUpdateResult
    {
        if (($callback = $artifact->getCustomSourceCheckUpdateCallback()) !== null) {
            return ApplicationContext::invoke($callback, [
                ArtifactDownloader::class => $this,
                'old_version' => null,
            ]);
        }
        $config = $artifact->getDownloadConfig('source');
        if (!is_array($config)) {
            return null;
        }
        $cls = $this->downloaders[$config['type']] ?? null;
        if (!is_a($cls, CheckUpdateInterface::class, true)) {
            return null;
        }
        /** @var CheckUpdateInterface $dl */
        $dl = new $cls();
        return $dl->checkUpdate($artifact_name, $config, null, $this);
    }

    private function probeBinaryCheckUpdate(Artifact $artifact, string $artifact_name): ?CheckUpdateResult
    {
        // custom binary callback takes precedence over config-based binary
        if (($callback = $artifact->getCustomBinaryCheckUpdateCallback()) !== null) {
            return ApplicationContext::invoke($callback, [
                ArtifactDownloader::class => $this,
                'old_version' => null,
            ]);
        }
        $binary_config = $artifact->getDownloadConfig('binary');
        $platform_config = is_array($binary_config) ? ($binary_config[SystemTarget::getCurrentPlatformString()] ?? null) : null;
        if (!is_array($platform_config)) {
            return null;
        }
        $cls = $this->downloaders[$platform_config['type']] ?? null;
        if (!is_a($cls, CheckUpdateInterface::class, true)) {
            return null;
        }
        /** @var CheckUpdateInterface $dl */
        $dl = new $cls();
        return $dl->checkUpdate($artifact_name, $platform_config, null, $this);
    }

    private function downloadWithType(Artifact $artifact, int $current, int $total, bool $parallel = false): int
    {
        $queue = $this->generateQueue($artifact);
        // already downloaded
        if ($queue === []) {
            logger()->debug("Artifact '{$artifact->getName()}' is already downloaded, skipping.");
            return SPC_DOWNLOAD_STATUS_SKIPPED;
        }

        $try = false;
        $last_exception = null;
        foreach ($queue as $item) {
            try {
                $instance = null;
                $call = $this->downloaders[$item['config']['type']] ?? null;
                $type_display_name = match (true) {
                    $item['lock'] === 'source' && ($callback = $artifact->getCustomSourceCallback()) !== null => 'user defined source downloader',
                    $item['lock'] === 'binary' && ($callback = $artifact->getCustomBinaryCallback()) !== null => 'user defined binary downloader',
                    default => SPC_DOWNLOAD_TYPE_DISPLAY_NAME[$item['config']['type']] ?? $item['config']['type'],
                };
                $try_h = $try ? 'Try downloading' : 'Downloading';
                logger()->info("{$try_h} artifact '{$artifact->getName()}' {$item['display']} ...");
                if ($parallel === false && $this->interactive) {
                    InteractiveTerm::indicateProgress("[{$current}/{$total}] Downloading artifact " . ConsoleColor::green($artifact->getName()) . " {$item['display']} from {$type_display_name} ...");
                }
                // is valid download type
                if ($item['lock'] === 'source' && ($callback = $artifact->getCustomSourceCallback()) !== null) {
                    $lock = ApplicationContext::invoke($callback, [
                        Artifact::class => $artifact,
                        ArtifactDownloader::class => $this,
                    ]);
                } elseif ($item['lock'] === 'binary' && ($callback = $artifact->getCustomBinaryCallback()) !== null) {
                    $lock = ApplicationContext::invoke($callback, [
                        Artifact::class => $artifact,
                        ArtifactDownloader::class => $this,
                    ]);
                } elseif (is_a($call, DownloadTypeInterface::class, true)) {
                    $instance = new $call();
                    $lock = $instance->download($artifact->getName(), $item['config'], $this);
                } else {
                    if ($item['config']['type'] === 'custom') {
                        $msg = "Artifact [{$artifact->getName()}] has no valid custom " . SystemTarget::getCurrentPlatformString() . ' download callback defined.';
                    } else {
                        $msg = "Artifact has invalid download type '{$item['config']['type']}' for {$item['display']}.";
                    }
                    throw new ValidationException($msg);
                }
                if (!$lock instanceof DownloadResult) {
                    throw new ValidationException("Artifact {$artifact->getName()} has invalid custom return value. Must be instance of DownloadResult.");
                }
                // verifying hash if possible
                $hash_validator = $instance ?? null;
                $verified = $lock->verified;
                if ($hash_validator instanceof ValidatorInterface) {
                    if (!$hash_validator->validate($artifact->getName(), $item['config'], $this, $lock)) {
                        throw new ValidationException("Hash validation failed for artifact '{$artifact->getName()}' {$item['display']}.");
                    }
                    $verified = true;
                }
                // process lock
                ApplicationContext::get(ArtifactCache::class)->lock($artifact, $item['lock'], $lock, SystemTarget::getCurrentPlatformString());
                if ($parallel === false && $this->interactive) {
                    $ver = $lock->hasVersion() ? (' (' . ConsoleColor::yellow($lock->version) . ')') : '';
                    InteractiveTerm::finish('Downloaded ' . ($verified ? 'and verified ' : '') . 'artifact ' . ConsoleColor::green($artifact->getName()) . $ver . " {$item['display']} .");
                }
                return SPC_DOWNLOAD_STATUS_SUCCESS;
            } catch (DownloaderException|ExecutionException $e) {
                if ($parallel === false && $this->interactive) {
                    InteractiveTerm::finish("Download artifact {$artifact->getName()} {$item['display']} failed !", false);
                    InteractiveTerm::error("Failed message: {$e->getMessage()}", true);
                }
                $last_exception = $e;
                $try = true;
                continue;
            } catch (ValidationException $e) {
                if ($parallel === false) {
                    InteractiveTerm::finish("Download artifact {$artifact->getName()} {$item['display']} failed !", false);
                    InteractiveTerm::error("Validation failed: {$e->getMessage()}");
                }
                $last_exception = $e;
                break;
            }
        }
        $vvv = !ApplicationContext::isDebug() ? "\nIf the problem persists, consider using `-v`, `-vv` or `-vvv` to enable verbose mode, or disable parallel downloading for more details." : '';
        throw new DownloaderException("Download artifact '{$artifact->getName()}' failed. Please check your internet connection and try again.{$vvv}", previous: $last_exception, artifact_name: $artifact->getName());
    }

    private function downloadWithConcurrency(): void
    {
        $skipped = [];
        $fiber_pool = [];
        $old_verbosity = null;
        $old_debug = null;
        try {
            $count = count($this->artifacts);
            // must mute
            $output = ApplicationContext::get(OutputInterface::class);
            if ($output->isVerbose()) {
                $old_verbosity = $output->getVerbosity();
                $old_debug = ApplicationContext::isDebug();
                logger()->warning('Parallel download is not supported in verbose mode, I will mute the output temporarily.');
                $output->setVerbosity(OutputInterface::VERBOSITY_NORMAL);
                ApplicationContext::setDebug(false);
                logger()->setLevel(LogLevel::ERROR);
            }
            $pool_count = $this->parallel;
            $downloaded = 0;
            $total = count($this->artifacts);

            Shell::passthruCallback(function () {
                InteractiveTerm::advance();
                \Fiber::suspend();
            });

            InteractiveTerm::indicateProgress("[{$downloaded}/{$total}] Downloading artifacts with concurrency {$this->parallel} ...");
            while (true) {
                // fill pool
                while (count($fiber_pool) < $pool_count && ($artifact = array_shift($this->artifacts)) !== null) {
                    $current = $count - count($this->artifacts);
                    $fiber = new \Fiber(function () use ($artifact, $current, $count) {
                        return [$artifact, $this->downloadWithType($artifact, $current, $count, true)];
                    });
                    $fiber->start();
                    $fiber_pool[] = $fiber;
                }
                // check pool
                foreach ($fiber_pool as $index => $fiber) {
                    if ($fiber->isTerminated()) {
                        try {
                            [$artifact, $int] = $fiber->getReturn();
                            if ($int === SPC_DOWNLOAD_STATUS_SKIPPED) {
                                $skipped[] = $artifact->getName();
                            }
                        } catch (\Throwable $e) {
                            $artifact_name = 'unknown';
                            if (isset($artifact)) {
                                $artifact_name = $artifact->getName();
                            }
                            logger()->debug("Download failed for artifact '{$artifact_name}': {$e->getMessage()}");
                            throw $e;
                        }
                        // remove from pool
                        unset($fiber_pool[$index]);
                        ++$downloaded;
                        InteractiveTerm::setMessage("[{$downloaded}/{$total}] Downloading artifacts with concurrency {$this->parallel} ...");
                        InteractiveTerm::advance();
                    } else {
                        $fiber->resume();
                    }
                }
                // all done
                if (count($this->artifacts) === 0 && count($fiber_pool) === 0) {
                    $skip_msg = !empty($skipped) ? ' (Skipped ' . count($skipped) . ' artifacts for being already downloaded)' : '';
                    InteractiveTerm::finish("Downloaded all {$total} artifacts.{$skip_msg}");
                    break;
                }
            }
        } catch (\Throwable $e) {
            // throw to all fibers to make them stop
            foreach ($fiber_pool as $fiber) {
                if (!$fiber->isTerminated()) {
                    try {
                        $fiber->throw($e);
                    } catch (\Throwable) {
                        // ignore errors when stopping fibers
                    }
                }
            }
            InteractiveTerm::finish('Parallel download failed !', false);
            throw $e;
        } finally {
            if ($old_verbosity !== null) {
                ApplicationContext::get(OutputInterface::class)->setVerbosity($old_verbosity);
                logger()->setLevel(match ($old_verbosity) {
                    OutputInterface::VERBOSITY_VERBOSE => LogLevel::INFO,
                    OutputInterface::VERBOSITY_VERY_VERBOSE, OutputInterface::VERBOSITY_DEBUG => LogLevel::DEBUG,
                    default => LogLevel::WARNING,
                });
            }
            if ($old_debug !== null) {
                ApplicationContext::setDebug($old_debug);
            }
            Shell::passthruCallback(null);
        }
    }

    /**
     * Generate download queue based on type preference.
     */
    private function generateQueue(Artifact $artifact): array
    {
        /** @var array<array{display: string, lock: string, config: array}> $queue */
        $queue = [];
        $binary_downloaded = $artifact->isBinaryDownloaded(compare_hash: true);
        $source_downloaded = $artifact->isSourceDownloaded(compare_hash: true);

        $item_source = ['display' => 'source', 'lock' => 'source', 'config' => $artifact->getDownloadConfig('source')];
        $item_source_mirror = ['display' => 'source (mirror)', 'lock' => 'source', 'config' => $artifact->getDownloadConfig('source-mirror')];

        // For binary config, handle both array configs and custom callbacks
        $binary_config = $artifact->getDownloadConfig('binary');
        $has_custom_binary = $artifact->getCustomBinaryCallback() !== null;
        $item_binary_config = null;
        if (is_array($binary_config)) {
            $item_binary_config = $binary_config[SystemTarget::getCurrentPlatformString()] ?? null;
        } elseif ($has_custom_binary) {
            // For custom binaries, create a dummy config to allow queue generation
            $item_binary_config = ['type' => 'custom'];
        }
        $item_binary = ['display' => 'binary', 'lock' => 'binary', 'config' => $item_binary_config];

        $binary_mirror_config = $artifact->getDownloadConfig('binary-mirror');
        $item_binary_mirror_config = null;
        if (is_array($binary_mirror_config)) {
            $item_binary_mirror_config = $binary_mirror_config[SystemTarget::getCurrentPlatformString()] ?? null;
        }
        $item_binary_mirror = ['display' => 'binary (mirror)', 'lock' => 'binary', 'config' => $item_binary_mirror_config];

        $pref = $this->fetch_prefs[$artifact->getName()] ?? $this->default_fetch_pref;
        if ($pref === Artifact::FETCH_PREFER_SOURCE) {
            $queue[] = $item_source['config'] !== null ? $item_source : null;
            $queue[] = $item_source_mirror['config'] !== null && $this->alt ? $item_source_mirror : null;
            $queue[] = $item_binary['config'] !== null ? $item_binary : null;
            $queue[] = $item_binary_mirror['config'] !== null && $this->alt ? $item_binary_mirror : null;
        } elseif ($pref === Artifact::FETCH_PREFER_BINARY) {
            $queue[] = $item_binary['config'] !== null ? $item_binary : null;
            $queue[] = $item_binary_mirror['config'] !== null && $this->alt ? $item_binary_mirror : null;
            $queue[] = $item_source['config'] !== null ? $item_source : null;
            $queue[] = $item_source_mirror['config'] !== null && $this->alt ? $item_source_mirror : null;
        } elseif ($pref === Artifact::FETCH_ONLY_SOURCE) {
            $queue[] = $item_source['config'] !== null ? $item_source : null;
            $queue[] = $item_source_mirror['config'] !== null && $this->alt ? $item_source_mirror : null;
        } elseif ($pref === Artifact::FETCH_ONLY_BINARY) {
            $queue[] = $item_binary['config'] !== null ? $item_binary : null;
            $queue[] = $item_binary_mirror['config'] !== null && $this->alt ? $item_binary_mirror : null;
        }
        // filter nulls
        $queue = array_values(array_filter($queue));

        // always download
        if ($this->ignore_cache === true || is_array($this->ignore_cache) && in_array($artifact->getName(), $this->ignore_cache)) {
            // validate: ensure at least one download source is available
            if (empty($queue)) {
                throw new ValidationException("Artifact '{$artifact->getName()}' does not provide any download source for current platform (" . SystemTarget::getCurrentPlatformString() . ').');
            }
            return $queue;
        }

        // check if already downloaded
        $has_usable_download = false;
        if ($pref === Artifact::FETCH_PREFER_SOURCE) {
            // prefer source: check source first, if not available check binary
            $has_usable_download = $source_downloaded || $binary_downloaded;
        } elseif ($pref === Artifact::FETCH_PREFER_BINARY) {
            // prefer binary: check binary first, if not available check source
            $has_usable_download = $binary_downloaded || $source_downloaded;
        } elseif ($pref === Artifact::FETCH_ONLY_SOURCE) {
            // source-only: only check if source is downloaded
            $has_usable_download = $source_downloaded;
        } elseif ($pref === Artifact::FETCH_ONLY_BINARY) {
            // binary-only: only check if binary for current platform is downloaded
            $has_usable_download = $binary_downloaded;
        }

        // if already downloaded, skip
        if ($has_usable_download) {
            return [];
        }

        // validate: ensure at least one download source is available
        if (empty($queue)) {
            if ($pref === Artifact::FETCH_ONLY_SOURCE) {
                throw new ValidationException("Artifact '{$artifact->getName()}' does not provide source download, cannot use --source-only mode.");
            }
            if ($pref === Artifact::FETCH_ONLY_BINARY) {
                throw new ValidationException("Artifact '{$artifact->getName()}' does not provide binary download for current platform (" . SystemTarget::getCurrentPlatformString() . '), cannot use --binary-only mode.');
            }
            // prefer modes should also throw error if no download source available
            throw new ValidationException("Validation failed: Artifact '{$artifact->getName()}' does not provide any download source for current platform (" . SystemTarget::getCurrentPlatformString() . ').');
        }

        return $queue;
    }

    private function applyCustomDownloads(): void
    {
        foreach ($this->custom_urls as $artifact_name => $custom_url) {
            if (isset($this->artifacts[$artifact_name])) {
                $this->artifacts[$artifact_name]->setCustomSourceCallback(function (ArtifactDownloader $downloader) use ($artifact_name, $custom_url) {
                    return (new Url())->download($artifact_name, ['url' => $custom_url], $downloader);
                });
            }
        }
        foreach ($this->custom_gits as $artifact_name => [$branch, $git_url]) {
            if (isset($this->artifacts[$artifact_name])) {
                $this->artifacts[$artifact_name]->setCustomSourceCallback(function (ArtifactDownloader $downloader) use ($artifact_name, $branch, $git_url) {
                    return (new Git())->download($artifact_name, ['rev' => $branch, 'url' => $git_url], $downloader);
                });
            }
        }
        foreach ($this->custom_locals as $artifact_name => $local_path) {
            if (isset($this->artifacts[$artifact_name])) {
                $this->artifacts[$artifact_name]->setCustomSourceCallback(function (ArtifactDownloader $downloader) use ($artifact_name, $local_path) {
                    return (new LocalDir())->download($artifact_name, ['dirname' => $local_path], $downloader);
                });
            }
        }
    }
}
