<?php

declare(strict_types=1);

namespace StaticPHP\Package;

use StaticPHP\Config\PackageConfig;
use StaticPHP\DI\ApplicationContext;
use StaticPHP\Exception\SPCException;
use StaticPHP\Exception\SPCInternalException;
use StaticPHP\Exception\WrongUsageException;
use StaticPHP\Runtime\Shell\Shell;
use StaticPHP\Runtime\SystemTarget;
use StaticPHP\Util\FileSystem;
use StaticPHP\Util\GlobalPathTrait;
use StaticPHP\Util\InteractiveTerm;
use StaticPHP\Util\System\LinuxUtil;

class PackageBuilder
{
    use GlobalPathTrait;

    /** @var int make jobs count */
    public readonly int $concurrency;

    /** @var array<string, mixed> Build arguments */
    protected array $arguments = [];

    /**
     * @param array $options Builder options
     */
    public function __construct(protected array $options = [])
    {
        ApplicationContext::set(PackageBuilder::class, $this);

        $this->concurrency = (int) getenv('SPC_CONCURRENCY') ?: 1;
    }

    public function buildPackage(Package $package, bool $force = false): int
    {
        // init build dirs
        if (!$package instanceof LibraryPackage) {
            throw new SPCInternalException('Please, never try to build non-library packages directly.');
        }
        FileSystem::createDir($package->getBuildRootPath());
        FileSystem::createDir($package->getIncludeDir());
        FileSystem::createDir($package->getBinDir());
        FileSystem::createDir($package->getLibDir());

        if (!$package->hasStage('build')) {
            throw new WrongUsageException("Package '{$package->name}' does not have a current platform 'build' stage defined.");
        }

        // validate package should be built
        if (!$force) {
            return $package->isInstalled() ? SPC_STATUS_ALREADY_BUILT : $this->buildPackage($package, true);
        }
        // check source is ready
        if ($package->getType() !== 'virtual-target' && !is_dir($package->getSourceDir())) {
            throw new WrongUsageException("Source directory for package '{$package->name}' does not exist. Please fetch the source before building.");
        }
        Shell::passthruCallback(function () {
            InteractiveTerm::advance();
        });

        try {
            if ($package->getType() !== 'virtual-target') {
                // patch before build
                $package->emitPatchBeforeBuild();
            }

            // build
            $package->runStage('build');

            if ($package->getType() !== 'virtual-target') {
                // install license
                if (($license = PackageConfig::get($package->getName(), 'license')) !== null) {
                    $this->installLicense($package, $license);
                }
            }
        } catch (SPCException $e) {
            // Ensure package information is bound if not already
            if ($e->getPackageInfo() === null) {
                $e->bindPackageInfo([
                    'package_name' => $package->name,
                    'package_type' => $package->type,
                    'package_class' => get_class($package),
                    'file' => null,
                    'line' => null,
                ]);
            }
            throw $e;
        }
        return SPC_STATUS_BUILT;
    }

    public function getOptions(): array
    {
        return $this->options;
    }

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

    public function setArgument(string $key, mixed $value): void
    {
        $this->arguments[$key] = $value;
    }

    public function getArgument(string $key, mixed $default = null): mixed
    {
        return $this->arguments[$key] ?? $default;
    }

    /**
     * Deploy the binary file from src to dst.
     */
    public function deployBinary(string $src, string $dst, bool $executable = true): string
    {
        logger()->debug("Deploying binary from {$src} to {$dst}");

        // file must exists
        if (!file_exists($src)) {
            throw new SPCInternalException("Deploy failed. Cannot find file: {$src}");
        }
        // dst dir must exists
        FileSystem::createDir(dirname($dst));

        // ignore copy to self
        if (realpath($src) !== realpath($dst)) {
            FileSystem::copy($src, $dst);
            if ($executable) {
                chmod($dst, 0755);
            }
        }

        // file exist
        if (!file_exists($dst)) {
            throw new SPCInternalException("Deploy failed. Cannot find file after copy: {$dst}");
        }

        if (!$this->getOption('no-strip') && SystemTarget::isUnix()) {
            // extract debug info
            $this->extractDebugInfo($dst);
            // strip binary
            $this->stripBinary($dst);
        }

        // UPX for linux
        $upx_option = $this->getOption('with-upx-pack');
        if ($upx_option && SystemTarget::getTargetOS() === 'Linux' && $executable) {
            if ($this->getOption('no-strip')) {
                logger()->warning('UPX compression is not recommended when --no-strip is enabled.');
            }
            logger()->info("Compressing {$dst} with UPX");
            shell()->exec(getenv('UPX_EXEC') . " --best {$dst}");
        } elseif ($upx_option && SystemTarget::getTargetOS() === 'Windows' && $executable) {
            logger()->info("Compressing {$dst} with UPX");
            cmd()->exec(getenv('UPX_EXEC') . ' --best ' . escapeshellarg($dst));
        }

        return $dst;
    }

    /**
     * Extract debug information from binary file.
     *
     * @param string $binary_path the path to the binary file, including executables, shared libraries, etc
     */
    public function extractDebugInfo(string $binary_path): string
    {
        $target_dir = BUILD_ROOT_PATH . '/debug';
        FileSystem::createDir($target_dir);
        $basename = basename($binary_path);
        $debug_file = "{$target_dir}/{$basename}" . (SystemTarget::getTargetOS() === 'Darwin' ? '.dwarf' : '.debug');
        if (SystemTarget::getTargetOS() === 'Darwin') {
            shell()->exec("dsymutil -f {$binary_path} -o {$debug_file}");
        } elseif (SystemTarget::getTargetOS() === 'Linux') {
            if ($eu_strip = LinuxUtil::findCommand('eu-strip')) {
                shell()
                    ->exec("{$eu_strip} -f {$debug_file} {$binary_path}")
                    ->exec("objcopy --add-gnu-debuglink={$debug_file} {$binary_path}");
            } else {
                shell()
                    ->exec("objcopy --only-keep-debug {$binary_path} {$debug_file}")
                    ->exec("objcopy --add-gnu-debuglink={$debug_file} {$binary_path}");
            }
        } else {
            logger()->debug('extractDebugInfo is only supported on Linux and macOS');
            return '';
        }
        return $debug_file;
    }

    /**
     * Strip unneeded symbols from binary file.
     */
    public function stripBinary(string $binary_path): void
    {
        shell()->exec(match (SystemTarget::getTargetOS()) {
            'Darwin' => "strip -S {$binary_path}",
            'Linux' => "strip --strip-unneeded {$binary_path}",
            'Windows' => 'echo "Skip strip on Windows"', // Windows strip is not available for now
            default => throw new SPCInternalException('stripBinary is only supported on Linux and macOS'),
        });
    }

    private function installLicense(Package $package, array $license): void
    {
        $dir = BUILD_ROOT_PATH . '/source-licenses/' . $package->getName();
        FileSystem::createDir($dir);
        if (is_assoc_array($license)) {
            $license = [$license];
        }

        foreach ($license as $index => $item) {
            if ($item['type'] === 'text') {
                FileSystem::writeFile("{$dir}/{$index}.txt", $item['text']);
            } elseif ($item['type'] === 'file') {
                FileSystem::copy("{$package->getSourceDir()}/{$item['path']}", "{$dir}/{$index}.txt");
            }
        }
    }
}
