#!/usr/bin/env php
<?php

declare(strict_types=1);

// Installs the drupack build for this host into the project root, from the
// release whose tag matches this package's installed version. The release's own
// install script downloads the build and checks its SHA-256.

use Composer\InstalledVersions;
use Composer\Semver\VersionParser;

const RELEASES = 'https://github.com/tresbientech/drupack/releases/download';

// A branch install has no release to download.
function releaseVersion(?string $installed): string
{
    try {
        (new VersionParser())->normalize((string) $installed);
        $release = VersionParser::parseStability((string) $installed) !== 'dev';
    } catch (UnexpectedValueException) {
        $release = false;
    }
    if (!$release) {
        throw new RuntimeException("drupal/drupack is installed at $installed, which names no release. "
            . 'Require a tagged release: composer require --dev drupal/drupack');
    }
    return $installed;
}

function executable(bool $windows): string
{
    return $windows ? 'drupack.exe' : 'drupack';
}

// The version comes from the project's lock, so it enters the URL encoded as
// one path segment.
function releaseUrl(string $version, string $file): string
{
    return RELEASES . '/' . rawurlencode($version) . "/$file";
}

function installCommand(bool $windows, string $script): array
{
    return $windows
        ? ['powershell', '-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', $script]
        : ['sh', $script];
}

// Runs the fetched script in $root and returns its exit status. The script
// inherits this process's environment, so a libc override reaches it. Its output
// tells how to move the executable onto PATH, which a project keeps in its root,
// so only its errors reach the terminal.
function runScript(bool $windows, string $script, string $root): int
{
    $pipes = [];
    $discard = ['file', $windows ? 'NUL' : '/dev/null', 'w'];
    return proc_close(proc_open(installCommand($windows, $script), [0 => STDIN, 1 => $discard, 2 => STDERR], $pipes, $root));
}

// Whether $binary is byte for byte a drupack build of $version, by the release's
// checksums.txt. The file is never run, since a project can hold any file there.
function isRelease(string $binary, string $checksums, string $version): bool
{
    $actual = hash_file('sha256', $binary);
    foreach (preg_split('/\R/', trim($checksums)) as $line) {
        [$sha256, $asset] = preg_split('/\s+/', trim($line), 2) + [1 => ''];
        if (str_starts_with($asset, "drupack-$version-") && hash_equals($sha256, $actual)) {
            return true;
        }
    }
    return false;
}

function download(string $url): string
{
    // The exception names the URL, which PHP's own warning would repeat.
    $contents = @file_get_contents($url);
    if ($contents === false) {
        throw new RuntimeException("Cannot download $url");
    }
    return $contents;
}

// Whether $directory or a parent holds a git repository.
function inRepository(string $directory): bool
{
    while (!file_exists("$directory/.git")) {
        if (dirname($directory) === $directory) {
            return false;
        }
        $directory = dirname($directory);
    }
    return true;
}

// Returns whether the line was added. A project outside git gets no .gitignore.
function ignore(string $root, string $name): bool
{
    $gitignore = "$root/.gitignore";
    if (!is_file($gitignore)) {
        if (!inRepository($root)) {
            return false;
        }
        touch($gitignore);
    }
    $lines = array_map('trim', file($gitignore));
    if (in_array("/$name", $lines, true) || in_array($name, $lines, true)) {
        return false;
    }
    $contents = file_get_contents($gitignore);
    $separator = $contents === '' || str_ends_with($contents, "\n") ? '' : "\n";
    file_put_contents($gitignore, "$separator/$name\n", FILE_APPEND);
    return true;
}

// application/tests/drupack_install_test.php loads this file for its functions alone.
if (defined('DRUPACK_INSTALL_LIBRARY')) {
    return;
}

require $GLOBALS['_composer_autoload_path'];

try {
    $windows = PHP_OS_FAMILY === 'Windows';
    $version = releaseVersion(InstalledVersions::getPrettyVersion('drupal/drupack'));
    $root = realpath(InstalledVersions::getRootPackage()['install_path']);
    $name = executable($windows);
    if (is_file("$root/$name") && isRelease("$root/$name", download(releaseUrl($version, 'checksums.txt')), $version)) {
        echo "$root/$name is drupack $version already.\n";
    } else {
        // PowerShell runs a file only under its .ps1 extension, so the temporary copy
        // keeps the release file's. The random name and the exclusive open keep a
        // planted file or link from standing in for it.
        $extension = $windows ? 'ps1' : 'sh';
        $contents = download(releaseUrl($version, "install-drupack.$extension"));
        $script = sys_get_temp_dir() . '/drupack-install-' . bin2hex(random_bytes(8)) . ".$extension";
        $handle = fopen($script, 'x');
        fwrite($handle, $contents);
        fclose($handle);
        echo "Downloading drupack $version for this system.\n";
        $status = runScript($windows, $script, $root);
        unlink($script);
        if ($status !== 0) {
            throw new RuntimeException("drupack $version was not installed.");
        }
        echo "Installed drupack $version in $root/$name.\n";
    }
    if (ignore($root, $name)) {
        echo "Added /$name to .gitignore.\n";
    }
    $start = $windows ? ".\\$name" : "./$name";
    echo "Serve this project with: $start\n";
    if (is_file("$root/vendor/drush/drush/drush.php")) {
        echo "Run its Drush on the same PHP with: $start drush status\n";
    }
} catch (Throwable $error) {
    fwrite(STDERR, $error->getMessage() . "\n");
    exit(1);
}
