<?php declare(strict_types=1);

namespace App;

error_reporting(error_level: E_ALL);

use DirectoryIterator;
use Exception;
use JetBrains\PhpStorm\NoReturn;
use PDO;
use PharData;


const SUPPORTED_RELEASE_MAJOR = 3;
const SUPPORTED_RELEASE_MINOR = 2;

/**
 *
 */
class UpdateController
{
    /**
     * @var true
     */
    private bool $dryRun = false;
    private string $installedVersion = '';

    function parseOpts(): array
    {
        $shortOpts = 'h::d::';

        $longOpts = [
                'help',
                'dry-run',
        ];

        return getopt(
                short_options: $shortOpts,
                long_options: $longOpts
        );
    }

    #[NoReturn]
    function handleUpdate(): void
    {
        define(constant_name: "PHPBB_ROOT_PATH", value: dirname(path: __DIR__, levels: 2));

        $phpBBRootPath = PHPBB_ROOT_PATH . '/';
        $phpEx = substr(string: strrchr(haystack: __FILE__, needle: '.'), offset: 1);


        // sanity checks

        if (strtoupper(string: substr(string: PHP_OS, offset: 0, length: 3)) === 'WIN') {
            echo 'This program must not be used on Windows installations.' . PHP_EOL;
            exit(1);
        }

        if (PHP_VERSION_ID < 80100) {
            echo 'You need at least php version 8.1.0';
            exit(1);
        }

        if (php_sapi_name() !== 'cli') {
            echo 'This program must be run from the command line.' . PHP_EOL;
            exit(1);
        }

        if (!extension_loaded(extension: 'bz2')) {
            echo 'You need to install/enable the bz2 PHP extension' . PHP_EOL;
            exit(1);
        }

        if (!$this->dryRun) {
            include $phpBBRootPath . 'config.php';

            /** @var String $dbhost */
            /** @var String $dbport */
            /** @var String $dbname */
            /** @var String $dbuser */
            /** @var String $dbpasswd */
            $pdo = new PDO(
                    dsn: "mysql:host=$dbhost;port=$dbport;charset=utf8mb4;dbname=$dbname",
                    username: $dbuser,
                    password: $dbpasswd
            );


            /** @var String $table_prefix */
            $sql = "SELECT config_value FROM {$table_prefix}config WHERE config_name = 'version'";
            $statement = $pdo->prepare(query: $sql);
            $statement->execute();
            $result = $statement->fetch();

            $installedVersion = $result['config_value'];
            print("Installed version: phpBB $installedVersion" . PHP_EOL);

            // phpBB has major, minor and maintenance version scheme
            [$major, $minor, $patch] = explode(separator: '.', string: $installedVersion);
            if ((intval(value: $major) != SUPPORTED_RELEASE_MAJOR) || (intval(value: $minor) < SUPPORTED_RELEASE_MINOR)) {
                echo 'This script only supports phpBB ' . SUPPORTED_RELEASE_MAJOR . '.' . SUPPORTED_RELEASE_MINOR . ' and above branch.', PHP_EOL;
                exit(1);
            }
        }

        echo "Checking for the current version …" . PHP_EOL;
        $json = file_get_contents(filename: 'https://version.phpbb.com/phpbb/versions.json');
        $versions = json_decode(json: $json, associative: true);

        $stableVersions = $versions['stable'];

        // Get the highest stable version
        $highestStableVersion = null;
        foreach ($stableVersions as $version => $details) {
            if ($highestStableVersion === null || version_compare(version1: $version, version2: $highestStableVersion, operator: '>')) {
                $highestStableVersion = $version;
            }
        }

        echo "Highest Stable Version: $highestStableVersion" . PHP_EOL;
        if (!$this->dryRun) {
            echo 'Installed Version: '. $this->installedVersion . PHP_EOL;
        }

        [$major, $minor, $patch] = explode(separator: '.', string: $stableVersions[$highestStableVersion]['current']);
        echo "Latest stable release: $major.$minor.$patch" . PHP_EOL;
        $availableUpdate = $stableVersions[$highestStableVersion]['current'];
        // check for existing update
        if (!file_exists(filename: 'dist')) {
            echo "'dist' folder is missing, create a new one …'";
            mkdir(directory: 'dist');
        }

        $currentFile = "phpBB-$availableUpdate.tar.bz2";

        $phpBBTarget = "dist/$currentFile";

        if (!file_exists(filename: $phpBBTarget)) {
            echo "Downloading $currentFile" . PHP_EOL;
            $filePath = "https://download.phpbb.com/pub/release/$major.$minor/$availableUpdate/$currentFile";
            $phpBBtbz = file_get_contents(filename: $filePath);
            file_put_contents(filename: $phpBBTarget, data: $phpBBtbz);
        } else {
            echo $currentFile . ' already exists. Skipping download.' . PHP_EOL;
        }

        // TODO check SHA256?

        // check for available language files

        $useLangDeDu = false;
        if (file_exists(filename: $phpBBRootPath . 'language/de')) {
            $useLangDeDu = true;
            $languageFile = "phpBB_lang_de-$availableUpdate.tar.bz2";
            $langDeDuTarget = "dist/$languageFile";

            if (!file_exists(filename: $langDeDuTarget)) {
                echo " Downloading $languageFile" . PHP_EOL;
                $filePath = "https://downloads.phpbb.de/pakete/deutsch/$major.$minor/$availableUpdate/$languageFile";

                if ($phpBBtbz = file_get_contents(filename: $filePath)) {
                    file_put_contents(filename: $langDeDuTarget, data: $phpBBtbz);
                } else {
                    echo " Language file $languageFile does not exist." . PHP_EOL;
                    $useLangDeDu = false;
                }
            } else {
                echo ' Language file ' . $languageFile . ' already exists' . PHP_EOL;
            }
        } else {
            echo 'Language Deutsch "Du" ist not installed, skipping' . PHP_EOL;
        }

        $useLangDeSie = false;
        if (file_exists(filename: $phpBBRootPath . 'language/de_x_sie')) {
            $useLangDeSie = true;
            $languageFile = "phpBB_lang_de_x_sie-$availableUpdate.tar.bz2";
            $langDeSieTarget = "dist/$languageFile";

            if (!file_exists(filename: $langDeSieTarget)) {
                echo " Downloading language $languageFile" . PHP_EOL;
                $filePath = "https://downloads.phpbb.de/pakete/deutsch/$major.$minor/$availableUpdate/$languageFile";

                if ($phpBBtbz = file_get_contents(filename: $filePath)) {
                    file_put_contents(filename: $langDeSieTarget, data: $phpBBtbz);
                } else {
                    echo ' Language file ' . $languageFile . ' does not exist' . PHP_EOL;
                    $useLangDeSie = false;
                }
            } else {
                echo ' Language file ' . $languageFile . ' already exists' . PHP_EOL;
            }
        } else {
            echo 'Language Deutsch "Sie" ist not installed, skipping' . PHP_EOL;
        }

        if (!$this->confirm(message: 'Do you want to proceed with the update now?')) {
            exit(0);
        }

        if ($this->dryRun) {
            echo 'Dry run, exiting.' . PHP_EOL;
            exit(0);
        }

        // ok, start update
        $now = date(format: 'd.m.Y H:i');
        $disableMsg = "Software-update at $now, the forum ist down due to maintenance. We'll be back soon.";
        $sql = "UPDATE {$table_prefix}config SET config_value = :disable_message WHERE config_name = 'board_disable_msg'";
        $statement = $pdo->prepare(query: $sql);
        $statement->bindParam(param: 'disable_message', var: $disableMsg);

        if ($statement->execute()) {
            echo "Disable Message set …", PHP_EOL;
        } else {
            echo 'There was an error talking to the DB.' . PHP_EOL;
            echo 'Failed SQL-statement: ' . $sql . PHP_EOL;
            exit(1);
        }

        $sql = "UPDATE {$table_prefix}config SET config_value = '1' WHERE config_name = 'board_disable'";
        $statement = $pdo->prepare(query: $sql);
        if ($statement->execute()) {
            echo "Board disabled …", PHP_EOL;
        } else {
            echo 'There was an error talking to the DB.' . PHP_EOL;
            echo 'Failed SQL-statement: ' . $sql . PHP_EOL;
            exit(1);
        }

        $extensionsFile = 'extensions.txt';

        if (file_exists(filename: $extensionsFile)) {
            echo 'Extensions state already stored. Remove extensions.txt if you wish to recreate it.' . PHP_EOL;
        } else {
            // check for enabled extensions
            $sql = "SELECT ext_name FROM {$table_prefix}ext WHERE ext_active = '1'";
            $statement = $pdo->prepare(query: $sql);
            if ($statement->execute()) {
                $result = $statement->fetchAll();

                if (count(value: $result) > 0) {
                    $extensions = json_encode(value: $result);

                    // safe enabled extensions
                    $oFile = fopen(filename: 'extensions.txt', mode: 'w');
                    fputs(stream: $oFile, data: $extensions);
                    fclose(stream: $oFile);
                    echo 'Stored extensions state';

                    // disable all extensions
                    $sql = "UPDATE {$table_prefix}ext SET ext_active = '0' WHERE ext_active = '1'";
                    $statement = $pdo->prepare(query: $sql);
                    if ($statement->execute()) {
                        echo 'Disabled all extensions';
                    }
                }

            }
        }

        $stylesFile = 'styles.txt';
        if (file_exists(filename: $stylesFile)) {
            echo 'Styles state already stored. Remove styles.txt if you wish to recreate it' . PHP_EOL;
        } else {
            // check for enabled style
            $sql = "SELECT style_name FROM {$table_prefix}styles WHERE style_active = '1'";
            $statement = $pdo->prepare(query: $sql);
            if ($statement->execute()) {
                $result = $statement->fetchAll();

                if (count(value: $result) > 0) {
                    $styles = json_encode(value: $result);

                    // safe enabled styles
                    $oFile = fopen(filename: 'styles.txt', mode: 'w');
                    fputs(stream: $oFile, data: $styles);
                    fclose(stream: $oFile);
                    echo 'Stored styles state.' . PHP_EOL;

                    // disable all styles except prosilver
                    $sql = "UPDATE {$table_prefix}styles SET style_active = '0' WHERE NOT style_name = 'prosilver'";
                    $statement = $pdo->prepare(query: $sql);
                    if ($statement->execute()) {
                        echo 'Disabled all styles except prosilver.' . PHP_EOL;
                    }
                }

            }
        }

        // update phpBB
        $data = new PharData(filename: $phpBBTarget);
        try {
            unset($data['phpBB3/config.php']);
            unset($data['phpBB3/.htaccess']);
        } catch (Exception $e) {
            echo 'error: ', $e;
        }

        // remove all old files
        $excludes = [
                'config.php',
                '.htaccess',
                '.htpasswd',
                'images',
                'files',
                'ext',
                'styles',
                'store',
                'updates',
                'mobiquo'];
        // this will fuck up nearly all modified boards, leave the files alone by default.
        // It will also destroy any API-keys and whatever you might have in your document root.
        //         deleteDirectory($phpbb_root_path, $excludes);


        try {
            $data->extractTo(directory: $phpBBRootPath);

            $fromDir = $phpBBRootPath . 'phpBB3/';
            $toDir = $phpBBRootPath;

            $this->copyDirectory(source: $fromDir, target: $toDir);
            $this->deleteDirectory(dir: $fromDir);
        } catch (Exception $e) {
            print("Error while extracting $data: $e");
            exit(1);
        }
        echo 'Moved the update in place.', PHP_EOL;

        $fileOwner = fileowner(filename: $phpBBRootPath);
        $fileGroup = filegroup(filename: $phpBBRootPath);
        $fileOwnerName = posix_getpwuid(user_id: $fileOwner)['name'];
        $fileGroupName = posix_getgrgid(group_id: $fileGroup)['name'];


        echo 'Check file owner', PHP_EOL;
        print("You might need to perform 'chown -R $fileOwnerName:$fileGroupName $phpBBRootPath'" . PHP_EOL);

        echo 'prepare config.yml.', PHP_EOL;
        $oFile = fopen(filename: $phpBBRootPath . '/update-config.yml', mode: 'w');
        fputs(stream: $oFile, data: 'updater:' . PHP_EOL . "        type: db_only" . PHP_EOL);
        fclose(stream: $oFile);

        $command = <<<EOC
		cd ..
		php install/phpbbcli.php update update-config.yml
		EOC;

        system(command: $command, result_code: $resultCode);

        if ($resultCode != 0) {
            echo 'There was an error updating the database: ' . $resultCode . PHP_EOL;
            exit(1);
        } else {
            echo 'The database has been updated' . PHP_EOL;
        }

        $installDir = $phpBBRootPath . '/install';
        if (is_dir(filename: $installDir)) {
            $this->deleteDirectory(dir: $installDir);
        }


        // update langDeDu
        if ($useLangDeDu) {
            $data = new PharData(filename: $langDeDuTarget);

            try {
                $data->extractTo(directory: $phpBBRootPath, overwrite: true);
            } catch (Exception $e) {
                print("Error while extracting $langDeDuTarget: $e");
                exit(1);
            }
        }


        // update langDeSie
        if ($useLangDeSie) {
            $data = new PharData(filename: $langDeSieTarget);

            try {
                $data->extractTo(directory: $phpBBRootPath, overwrite: true);
            } catch (Exception $e) {
                print("Error while extracting $langDeSieTarget: $e");
                exit(1);
            }
        }


        $sql = "UPDATE {$table_prefix}config SET config_value = '0' WHERE config_name = 'board_disable'";
        $statement = $pdo->prepare(query: $sql);
        $statement->execute();

        echo "Board reenabled …", PHP_EOL;


        if (file_exists(filename: $extensionsFile)) {
            $iFile = fopen(filename: $extensionsFile, mode: 'r');
            $extensions = json_decode(json: fgets(stream: $iFile), associative: true);

            echo 'Enable extensions: ';

            foreach ($extensions as $extension) {
                $ext = $extension['ext_name'];
                $sql = "UPDATE {$table_prefix}ext SET ext_active = '1' WHERE ext_name = '$ext'";
                $statement = $pdo->prepare(query: $sql);
                $statement->execute();
                echo '.';
            }
            echo 'done.', PHP_EOL;
        } else {
            echo 'There are no saved extension information available.', PHP_EOL;
        }

        if (file_exists(filename: $stylesFile)) {
            $iFile = fopen(filename: $stylesFile, mode: 'r');
            $styles = json_decode(json: fgets(stream: $iFile), associative: true);

            echo 'Enable styles: ';

            foreach ($styles as $style) {
                $style = $style['style_name'];
                $sql = "UPDATE {$table_prefix}styles SET style_active = '1' WHERE style_name = '$style'";
                $statement = $pdo->prepare(query: $sql);
                $statement->execute();
                echo '.';
            }
            echo 'done.', PHP_EOL;
        } else {
            echo 'There are no saved extension information available.', PHP_EOL;
        }


        // clear cache
        $dataGlobalCache = $phpBBRootPath . '/cache/data_global.' . $phpEx;

        if (file_exists(filename: $dataGlobalCache)) {
            unlink(filename: $dataGlobalCache);
            echo "Cache cleared …";
        }

        echo "Your board should now be up and running." . PHP_EOL;
    }


    /**
     * @param String $message
     * @param string[] $options
     * @param string $default
     *
     * @return bool
     */
    function confirm(string $message = 'Are you sure? ', array $options = ['y', 'n'], string $default = 'n'): bool
    {
        // first $options means true, any other false

        echo $message, ' (';
        $first = true;
        foreach ($options as $option) {
            // mark default
            if ($option == $default) {
                $option = strtoupper(string: $option);
            }
            if ($first) {
                echo $option;
                $first = false;
            } else {
                echo '/', $option;
            }
        }
        echo '): ';

        $handle = fopen(filename: "php://stdin", mode: 'r');
        $line = trim(string: fgetc(stream: $handle));
        fclose(stream: $handle);

        if ($line == '') {
            // enter
            $line = $default;
        }

        if ($line == $options[0]) {
            $result = true;
        } else {
            $result = false;
        }

        return $result;
    }


    /**
     * @param       $dir
     * @param array $excludes
     *
     * @return false|void
     */
    function deleteDirectory($dir, array $excludes = [])
    {
        if (!file_exists(filename: $dir)) {
            return false;
        }
        $dir = rtrim(string: $dir, characters: '/') . '/';
        static $skip = false;

        $entries = glob(pattern: $dir . '{,.}[!.,!..]*', flags: GLOB_MARK | GLOB_BRACE);

        foreach ($entries as $entry) {
            if (!in_array(needle: basename(path: $entry), haystack: $excludes)) {
                if (is_dir(filename: $entry)) {
                    $this->deleteDirectory(dir: $entry);
                } else {
                    unlink(filename: $entry);
                }
            } else {
                $skip = true;
            }
        }
        if (!$skip) {
            rmdir(directory: $dir);
        }
    }


    /**
     * @param $source
     * @param $target
     *
     * @return bool|void
     */
    function copyDirectory($source, $target)
    {
        if (!file_exists(filename: $source)) {
            die("missing source: $source");
        }

        if (is_file(filename: $source)) {
            if (copy(from: $source, to: $target)) {
                return true;
            } else {
                return false;
            }
        }

        if (is_dir(filename: $source)) {
            if (!file_exists(filename: $target)) {
                mkdir(directory: $target);
            }
            $source = rtrim(string: $source, characters: '/') . '/';
            $target = rtrim(string: $target, characters: '/') . '/';
            $dir = new DirectoryIterator(directory: $source);

            foreach ($dir as $entry) {
                if (!$entry->isDot()) {
                    $this->copyDirectory(source: "$source$entry", target: "$target$entry");
                }
            }
        }
        // ignore links, not part of phpBB arch
    }

    public function printHelp(): void
    {
        echo "Usage: php update.php [options]", PHP_EOL;
        echo "Options:", PHP_EOL;
        echo "-h --help    Print this help", PHP_EOL;
        echo "-d --dry-run Just check for downloadable files, don't connect to database.", PHP_EOL;
    }

    public function setDryRun(): void
    {
        $this->dryRun = true;
    }

}