<?php
/**
 * Setup Command Class
 *
 * @version     1.0.$Revision:$
 * @version     SVN: $Id:$
 * @package     bplan-base/globals
 * @subpackage  Commands
 * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
 * @copyright   Copyright (C) 2025, 2026 bplan-solutions GmbH & Co. KG <https://www.bplan-solutions.de/>
 * /Δ\
 */

namespace BplanBase\Globals\Console\Commands;


use BplanBase\Globals\Composer\ComposerJson;
use BplanBase\Globals\Console\BaseSetup;
use BplanBase\Globals\Env\EnvCrypt;
use BplanBase\Globals\Env\EnvFile;
use BplanBase\Globals\Json\JsonFile;
use BplanBase\Globals\Setup\FilePublisher;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Artisan;


use function Laravel\Prompts\clear;


/**
 * Setup Command Class
 *
 * @version     16.2.0 / 2026-05-15
 * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
 */
class GlobalsSetup extends BaseSetup
{


/* +++ TRAITS +++ +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ */


/* +++ CLASS CONSTANTS +++ ++++++++++++++++++++++++++++++++++++++++++++++++++ */


    /**
     * @var     string
     */
    const SCHEDULER_TASK_PREFIX = 'Laravel Scheduler';


    /**
     * Git checkout: .env.in exists, .env does not. Installation mode must be prompted.
     *
     * @var     string
     */
    const STATE_CHECKOUT   = 'checkout';


    /**
     * Setup was already run: both .env and .env.in exist. Mode is derived from APP_ENV.
     *
     * @var     string
     */
    const STATE_CONFIGURED = 'configured';


    /**
     * Fresh Laravel install: .env exists, .env.in does not. Mode defaults to local.
     *
     * @var     string
     */
    const STATE_FRESH      = 'fresh';


    /**
     * Neither .env nor .env.in found. Cannot proceed.
     *
     * @var     string
     */
    const STATE_INVALID    = 'invalid';


    /**
     * @var     array
     */
    const TIMEZONES = [
        'Europe/Berlin',
    ];


/* +++ OBJECT MEMBERS +++ +++++++++++++++++++++++++++++++++++++++++++++++++++ */


    /**
     * @var     string
     */
    private string $_apiPackage = 'laravel-json-api/laravel';


    /**
     * @var     string
     */
    private string $_codeGeneratorLoaderPackage = 'bplan-base/laravel-code-generator-loader';


    /**
     * @var     string
     */
    private string $_codeGeneratorPackage = 'bplan-base/laravel-code-generator';


    /**
     * @var     array
     */
    private array $_done = [];


    /**
     * @var     boolean
     */
    private bool $_hasApi;


    /**
     * @var     boolean
     */
    private bool $_hasCodeGenerator;


    /**
     * @var     boolean
     */
    private bool $_hasCodeGeneratorLoader;


    /**
     * @var     boolean
     */
    private bool $_hasHttpService;


    /**
     * @var     boolean
     */
    private bool $_hasLangPackage;


    /**
     * @var     boolean
     */
    private bool $_hasMergePlugin;


    /**
     * @var     boolean
     */
    private bool $_hasPrivateGit;


    /**
     * @var     string  'local' | 'dev-server' | 'production' | 'demo'
     */
    private string $_installationMode = 'local';


    /**
     * @var     string  One of the STATE_* constants.
     */
    private string $_installationState = self::STATE_INVALID;


    /**
     * @var     int
     */
    private int $_installPackages = 0;


    /**
     * @var     string
     */
    private string $_mergePlugin = 'wikimedia/composer-merge-plugin';


    /**
     * @var array
     */
    private array $_missingLocales = [];


    /**
     * @var     array
     */
    private array $_pending = [];


    /**
     * @var     string
     */
    private string $_privateGitName = 'bplan/composer-packages';


    /**
     * @var     string
     */
    private string $_privateGitUrl = 'https://satis.bplan.solutions';


    /**
     * @var     boolean
     */
    private bool $_requireApi = false;


    /**
     * @var     bool
     */
    private bool $_requireCodeGenerator = false;


    /**
     * @var     bool
     */
    private bool $_requireHttpService = false;


    /**
     * @var     bool
     */
    private bool $_wantsScheduledTask = false;


    /**
     * @var     string
     */
    protected $description = 'Prepares the Laravel project environment';


    /**
     * @var     string
     */
    protected $signature = 'globals:setup {--with-schedule : Set up a Windows Scheduled Task for the Laravel Scheduler}';


/* +++ CLASS MEMBERS +++ ++++++++++++++++++++++++++++++++++++++++++++++++++++ */


/* +++ OBJECT METHODS +++ +++++++++++++++++++++++++++++++++++++++++++++++++++ */


    /**
     *
     * @return      void
     *
     * @version     1.1.0 / 2026-04-23
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    private function _addLangUpdateCommand(): void
    {
        $command  = '@php artisan lang:publish';
        $Composer = ComposerJson::load(base_path('composer.json'));

        if ($Composer->hasScript('post-update-cmd', $command) === false) {
            $this->_Output->bulletMessage('Add [artisan lang:publish] to "scripts.post-update-cmd" in composer.json');

            $Composer->addScript('post-update-cmd', $command)->save();
        }
    } // _addLangUpdateCommand()


    /**
     * Ensures APP_KEY is set and prompts for APP_NAME, APP_URL and APP_LOCALE
     * when they still hold their default/placeholder values.
     *
     * Called after .env is guaranteed to exist, before database/mail configuration.
     *
     * @return      void
     *
     * @version     1.1.0 / 2026-04-23
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    private function _configureApp(): void
    {
        $envFile = base_path('.env');
        $Env     = EnvFile::load($envFile);

        /*
        **  APP_KEY — generate silently if missing or invalid. */
        $appKey = $Env->get('APP_KEY');

        if ($appKey === '' || str_starts_with($appKey, 'base64:') === false) {
            $this->_Output->caption('Application key');

            Artisan::call('key:generate');

            $this->_Output->bulletMessage(trim(Artisan::output()));
            $this->_done[] = 'APP_KEY generated';

            /*
            **  Reload: key:generate writes directly to the file. */
            $Env = EnvFile::load($envFile);
        }

        $changed = false;

        /*
        **  APP_NAME — prompt while still set to the Laravel default. */
        $appName = $Env->get('APP_NAME', 'Laravel');

        if ($appName === 'Laravel') {
            $this->_Output->caption('Application name');

            $name = $this->_Prompt->ask('APP_NAME', $appName);

            $Env->set('APP_NAME', '"'.$name.'"');
            $this->_done[] = 'APP_NAME set to "'.$name.'"';
            $changed       = true;
        }

        /*
        **  APP_URL — prompt while localhost is in the URL. */
        $appUrl = $Env->get('APP_URL', 'http://localhost');

        if (str_contains($appUrl, 'localhost') === true) {
            $this->_Output->caption('Application URL');

            $url = $this->_Prompt->ask('APP_URL', $appUrl);

            $Env->set('APP_URL', $url);
            $this->_done[] = 'APP_URL set to "'.$url.'"';
            $changed       = true;
        }

        /*
        **  APP_LOCALE — prompt while locale and fallback locale are identical. */
        $locale   = $Env->get('APP_LOCALE', 'en');
        $fallback = $Env->get('APP_FALLBACK_LOCALE', 'en');

        if ($locale === $fallback) {
            $this->_Output->caption('Application locale');

            $newLocale = $this->_Prompt->ask('APP_LOCALE', $locale);

            $Env->set('APP_LOCALE', $newLocale);
            $this->_done[] = 'APP_LOCALE set to "'.$newLocale.'"';
            $changed       = true;
        }

        if ($changed === true) {
            $Env->save();
        }
    } // _configureApp()


    /**
     * Asks the user for database connection settings and writes them to .env.
     *
     * Skipped when: user declines, or DB is already configured with a non-sqlite driver
     * and all required fields are filled. Re-asked when sqlite is active or required
     * fields are empty.
     *
     * @return      void
     *
     * @version     1.3.0 / 2026-04-26
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    private function _configureDatabase(): void
    {
        $Env        = EnvFile::loadOrCreate(base_path('.env'));
        $connection = $Env->get('DB_CONNECTION', 'sqlite');
        $host       = $Env->get('DB_HOST');
        $database   = $Env->get('DB_DATABASE');
        $username   = $Env->get('DB_USERNAME');

        /*
        **  Determine whether to prompt based on current state. */
        $needsConfig = $connection === 'sqlite'
            || $host === ''
            || $database === ''
            || $username === '';

        if ($needsConfig === false) {
            return;
        }
        if ($this->_Prompt->confirm('Do you want to configure the database connection?', true) === false) {
            $this->_pending[] = 'Configure database connection';

            return;
        }
        $this->_Output->caption('Database configuration');

        $driverIndex = (int) $this->_Prompt->choice(
            'Select database driver:',
            ['sqlite', 'mysql', 'pgsql', 'sqlsrv'],
            $connection === 'sqlite' ? 0 : (array_search($connection, ['sqlite', 'mysql', 'pgsql', 'sqlsrv']) ?: 1)
        );
        $driver = ['sqlite', 'mysql', 'pgsql', 'sqlsrv'][$driverIndex];

        if ($driver === 'sqlite') {
            $Env->set('DB_CONNECTION', $driver)
                ->set('DB_DATABASE',   'database/database.sqlite')
                ->save();

            $this->_done[] = 'Database configured (sqlite)';

            return;
        }
        $defaultPort = match ($driver) {
            'mysql'  => '3306',
            'pgsql'  => '5432',
            'sqlsrv' => '1433',
            default  => '3306',
        };

        $dbHost     = $this->_Prompt->ask('DB_HOST',     $host !== ''     ? $host     : '127.0.0.1');
        $dbDatabase = $this->_Prompt->ask('DB_DATABASE', $database !== '' ? $database : '');
        $dbPassword = $this->_Prompt->secret('DB_PASSWORD');
        $dbPort     = $this->_Prompt->ask('DB_PORT',     $Env->get('DB_PORT', $defaultPort));
        $dbUsername = $this->_Prompt->ask('DB_USERNAME', $username !== '' ? $username : '');

        $Env->set('DB_CONNECTION', $driver)
            ->set('DB_DATABASE',   $dbDatabase)
            ->set('DB_HOST',       $dbHost)
            ->set('DB_PASSWORD',   '"'.EnvCrypt::encrypt($dbPassword).'"')
            ->set('DB_PORT',       $dbPort)
            ->set('DB_USERNAME',   $dbUsername)
            ->save();

        $this->_done[] = 'Database configured ('.$driver.')';

    } // _configureDatabase()


    /**
     * Asks the user for mail settings and writes them to .env.
     *
     * Skipped when: user declines, or MAIL_HOST is already configured.
     * Re-asked when MAIL_HOST is empty.
     *
     * @return      void
     *
     * @version     1.2.0 / 2026-04-23
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    private function _configureMail(): void
    {
        $Env        = EnvFile::loadOrCreate(base_path('.env'));
        $mailHost   = $Env->get('MAIL_HOST');
        $mailMailer = $Env->get('MAIL_MAILER', 'smtp');

        if ($mailHost !== '' && $mailMailer !== 'log' && $mailMailer !== 'array') {
            return;
        }
        if ($this->_Prompt->confirm('Do you want to configure the mail connection?', false) === false) {
            $this->_pending[] = 'Configure mail connection';

            return;
        }
        $this->_Output->caption('Mail configuration');

        $mailers     = ['smtp', 'mailgun', 'ses', 'log', 'array'];
        $mailerIndex = (int) $this->_Prompt->choice(
            'Select mail driver:',
            $mailers,
            array_search($mailMailer, $mailers) ?: 0
        );
        $mailer = $mailers[$mailerIndex];

        if ($mailer !== 'log' && $mailer !== 'array') {
            $host       = $this->_Prompt->ask('MAIL_HOST',       $mailHost !== '' ? $mailHost : '');
            $port       = $this->_Prompt->ask('MAIL_PORT',       $Env->get('MAIL_PORT', '587'));
            $username   = $this->_Prompt->ask('MAIL_USERNAME',   $Env->get('MAIL_USERNAME'));
            $password   = $this->_Prompt->secret('MAIL_PASSWORD');
            $encryption = $this->_Prompt->ask('MAIL_ENCRYPTION', $Env->get('MAIL_ENCRYPTION', 'tls'));

            $Env->set('MAIL_HOST',       $host)
                ->set('MAIL_PORT',       $port)
                ->set('MAIL_USERNAME',   $username)
                ->set('MAIL_PASSWORD',   $password)
                ->set('MAIL_ENCRYPTION', $encryption);
        }
        $fromAddress = $this->_Prompt->ask('MAIL_FROM_ADDRESS', $Env->get('MAIL_FROM_ADDRESS'));
        $fromName    = $this->_Prompt->ask('MAIL_FROM_NAME',    trim($Env->get('MAIL_FROM_NAME'), '"'));

        $Env->set('MAIL_MAILER',       $mailer)
            ->set('MAIL_FROM_ADDRESS', $fromAddress)
            ->set('MAIL_FROM_NAME',    '"'.$fromName.'"')
            ->save();

        $this->_done[] = 'Mail configured ('.$mailer.')';

    } // _configureMail()


    /**
     * Inserts APP_TIMEZONE into .env when not yet present.
     *
     * When TIMEZONES holds a single entry it is applied automatically.
     * When multiple entries are present the user is prompted to choose.
     *
     * @return      void
     *
     * @version     1.0.0 / 2026-04-23
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    private function _configureTimezone(): void
    {
        $Env = EnvFile::load(base_path('.env'));

        if ($Env->has('APP_TIMEZONE') === true) {
            return;
        }
        if (count(self::TIMEZONES) === 1) {
            $timezone = self::TIMEZONES[0];
        } else {
            $this->_Output->caption('Application timezone');

            $index    = (int) $this->_Prompt->choice('Select APP_TIMEZONE:', self::TIMEZONES, 0);
            $timezone = self::TIMEZONES[$index];
        }
        $Env->insertInSection('APP_TIMEZONE', $timezone, 'GLOBAL')->save();

        $this->_done[] = 'APP_TIMEZONE set to "'.$timezone.'"';

    } // _configureTimezone()


    /**
     * Checks whether the current process has administrator privileges.
     *
     * Must be called early in handle() — before any interactive prompts — so the
     * user is not interrupted mid-setup by a missing-privilege error.
     *
     * Only relevant when _isProdMode() is true or --with-schedule was passed.
     * Skipped entirely when the scheduler task already belongs to this installation.
     *
     * @return      bool    false when admin rights are required but not present
     *
     * @version     1.0.0 / 2026-04-25
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    private function _checkSchedulerAdminRights(): bool
    {
        if ($this->_isProdMode() === false && $this->option('with-schedule') === false) {
            return true;
        }
        $taskName = $this->_resolveSchedulerTaskName();

        if ($this->_schedulerTaskBelongsHere($taskName) === true) {
            return true;
        }
        $this->_Output->caption('Windows Scheduled Task');
        $this->_Output->print(
            'Required for: scheduled commands (schedule:run), queued jobs, cache expiry, etc.',
            bullet: true
        );

        if ($this->_Prompt->confirm('Do you want to set up a Windows Scheduled Task for the Laravel Scheduler?', true) === false) {
            return true;
        }
        $this->_wantsScheduledTask = true;

        exec('net session >nul 2>&1', result_code: $exitCode);

        if ($exitCode !== 0) {
            $this->_Output->error('Administrator rights are required to create a scheduled task. Re-run as Administrator.');

            return false;
        }
        return true;

    } // _checkSchedulerAdminRights()


    /**
     *
     * @return 	    void
     *
     * @version     1.4.0 / 2026-04-27
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    private function _finishPackageSetup(): void
    {
        if ($this->_installPackages > 0) {
            $this->_Output->caption('Package installation');

            if ($this->_installationMode === 'local') {
                $this->_Output->print('Run `composer update`', bullet: true);

                $this->_runShellCommand(['composer', 'update']);
            } else {
                $this->_Output->print('Run `composer install`', bullet: true);

                $this->_runShellCommand(['composer', 'install']);
            }
            if ($this->_requireApi === true) {
                $Publisher = new FilePublisher($this->_basePath, ['etc/setup/src/config/jsonapi.php' => 'config/jsonapi.php'], [$this, 'publishMessage']);

                $Publisher->publish();
            }
            $this->_done[] = 'Packages installed via composer';
        }
        $this->_Output->caption('Update existing locales');

        if ($this->getApplication()->has('lang:update') === true) {
            $this->call('lang:update');
        }

        if (!empty($this->_missingLocales)) {
            $this->_Output->caption('Add new locales');

            foreach ($this->_missingLocales as $locale) {
                Artisan::call('lang:add '.$locale);
            }
        }
        if ($this->_hasLangPackage === true) {
            $this->_addLangUpdateCommand();
        }
    } // _finishPackageSetup()


    /**
     *
     * @return 	    void
     *
     * @version     1.7.0 / 2026-04-23
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    protected function _init(): void
    {
        parent::_init();

        $this->_basePath               = str_replace('\\', '/', realpath(__DIR__.'/../../../'));
        $this->_firstRun               = !file_exists(config_path('project.php'));
        $this->_hasApi                 = file_exists(base_path('vendor/'.$this->_apiPackage.'/core/composer.json'));
        $this->_hasCodeGenerator       = file_exists(base_path('vendor/'.$this->_codeGeneratorPackage.'/composer.json'));
        $this->_hasCodeGeneratorLoader = file_exists(base_path('vendor/'.$this->_codeGeneratorLoaderPackage.'/composer.json'));
        $this->_hasHttpService         = file_exists(config_path('http-service.php'));
        $this->_hasLangPackage         = file_exists(base_path('vendor/laravel-lang/lang/composer.json'));//composer require --dev laravel-lang/lang
        $this->_hasMergePlugin         = file_exists(base_path('vendor/'.$this->_mergePlugin.'/composer.json'));
        $this->_hasPrivateGit          = ComposerJson::load(base_path('composer.json'))->hasRepository($this->_privateGitName);

        $this->_publishFiles = config('globals-setup.publish', []);

        $hasEnv   = file_exists(base_path('.env'));
        $hasEnvIn = file_exists(base_path('.env.in'));

        $this->_installationState = match (true) {
            $hasEnv  && $hasEnvIn  => self::STATE_CONFIGURED,
            $hasEnv  && !$hasEnvIn => self::STATE_FRESH,
            !$hasEnv && $hasEnvIn  => self::STATE_CHECKOUT,
            default                => self::STATE_INVALID,
        };

        if ($this->_installationState === self::STATE_INVALID) {
            $this->_Output->error('Neither .env nor .env.in found. Cannot determine installation state.');
        }
        $this->_initMissingLocales();

    } // _init()


    /**
     *
     * @return      void
     *
     * @version     1.0.0 / 2026-02-28
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    private function _initMissingLocales()
    {
        foreach (config('globals-setup.locales') as $locale) {
            if (file_exists(lang_path($locale.'.json'))) {
                continue;
            }
            $this->_missingLocales[] = $locale;
        }
    } // _initMissingLocales()


    /**
     * @return      bool
     *
     * @version     1.0.0 / 2026-04-19
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    private function _isDevMode(): bool
    {
        return $this->_installationMode === 'local' || $this->_installationMode === 'dev-server';

    } // _isDevMode()


    /**
     * @return      bool
     *
     * @version     1.0.0 / 2026-04-19
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    private function _isProdMode(): bool
    {
        return $this->_installationMode === 'production' || $this->_installationMode === 'demo';

    } // _isProdMode()


    /**
     *
     * @return 	    void
     *
     * @version     1.1.0 / 2025-06-19
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    private function _linkStorage(): void
    {
        /*
        **  Storage-Link erzeugen. */
        $this->_Output->caption('Link storage directory');

        if (file_exists(public_path('storage'))) {
            $this->_Output->bulletMessage('Storage link already exists');

            return;
        }
        Artisan::call('storage:link');

        $this->_Output->bulletMessage(Artisan::output());

    } // _linkStorage()


    /**
     *
     * @return 	    void
     *
     * @version     1.6.1 / 2026-05-15
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    private function _prepareEnvFiles(): void
    {
        $envFile = base_path('.env');
        $inFile  = base_path('.env.in');

        if ($this->_installationMode !== 'local') {
            /*
            **  dev-server / production / demo: .env.in (committed template) → .env,
            **  then set APP_ENV/APP_DEBUG according to mode. */
            if (file_exists($envFile) === false) {
                $this->_Output->caption('Preparing configuration');
                $this->_Output->bulletMessage('Copy .env.in > .env');

                copy($inFile, $envFile);

                /*
                **  Generate a unique instance ID so the scheduler task name is
                **  distinct per installation. Never overwrite an existing value. */
                $Env = EnvFile::load($envFile);

                if ($Env->get('APP_INSTANCE_ID') === '') {
                    $id = substr(str_replace('-', '', (string) \Illuminate\Support\Str::uuid()), 0, 12);

                    $Env->set('APP_INSTANCE_ID', $id)->save();
                }
            }
            [$appEnv, $appDebug] = match ($this->_installationMode) {
                'production' => ['production', 'false'],
                'demo'       => ['demo',       'false'],
                default      => ['local',      'true'],  // dev-server
            };

            EnvFile::load($envFile)
                ->set('APP_ENV',   $appEnv)
                ->set('APP_DEBUG', $appDebug)
                ->save();

            return;
        }

        /*
        **  Local + STATE_CHECKOUT: .env.in exists but .env does not — copy template. */
        if (file_exists($envFile) === false) {
            $this->_Output->caption('Preparing configuration');
            $this->_Output->bulletMessage('Copy .env.in > .env');

            copy($inFile, $envFile);
        }

        /*
        **  Local: reorganize existing .env, copy to .env.in, append custom block. */
        if (file_exists($inFile) === false) {
            $this->_Output->caption('Preparing configuration');
            $this->_Output->bulletMessage('Prepare file .env');
            $this->_Output->bulletMessage('Copy .env > .env.in');

            EnvFile::load($envFile)->reorganize()->save();
            copy($envFile, $inFile);

            $this->_Output->bulletMessage('Extend file .env');

            $customBlock =
                "\n\n\n".'#[APP]'
                ."\n"
                ."\n".'APP_INSTANCE_ID = '.substr(str_replace('-', '', (string) \Illuminate\Support\Str::uuid()), 0, 12)
                ."\n\n\n".'#[CUSTOM]'
                ."\n"
                ."\n".'CODE_GENERATOR_ACTIVE = '.($this->_requireCodeGenerator ? 'true' : 'false')
                ."\n".'CODE_GENERATOR_BUILD_ONLY_MODE = true'
                ."\n".'CODE_GENERATOR_CREATE_JSONAPI = '.($this->_requireApi ? 'true' : 'false')
                ."\n".'CODE_GENERATOR_SKIP_ROUTING = true'
                ."\n"
                ."\n".'GLOBAL_TENANT = Customer'
                ."\n";

            $inCustomBlock =
                "\n\n\n".'#[APP]'
                ."\n"
                ."\n".'APP_INSTANCE_ID ='
                ."\n\n\n".'#[CUSTOM]'
                ."\n"
                ."\n".'CODE_GENERATOR_ACTIVE = false'
                ."\n".'CODE_GENERATOR_BUILD_ONLY_MODE = true'
                ."\n".'CODE_GENERATOR_CREATE_JSONAPI = '.($this->_requireApi ? 'true' : 'false')
                ."\n".'CODE_GENERATOR_SKIP_ROUTING = true'
                ."\n"
                ."\n".'GLOBAL_TENANT = Customer'
                ."\n";

            if ($this->_requireHttpService === true) {
                $customBlock   .= "\n".'HTTP_SERVICE_PROFILE_DEFAULT_BASE_URL = '."\n";
                $inCustomBlock .= "\n".'HTTP_SERVICE_PROFILE_DEFAULT_BASE_URL = '."\n";
            }
            file_put_contents($envFile, $customBlock, FILE_APPEND);

            $this->_Output->bulletMessage('Extend file .env.in');

            file_put_contents($inFile, $inCustomBlock, FILE_APPEND);

        } else {
            if ($this->_requireHttpService === true) {
                EnvFile::load($envFile)->insertInSection('HTTP_SERVICE_PROFILE_DEFAULT_BASE_URL', '', 'CUSTOM')->save();
                EnvFile::load($inFile)->insertInSection('HTTP_SERVICE_PROFILE_DEFAULT_BASE_URL', '', 'CUSTOM')->save();
            }
        }
    } // _prepareEnvFiles()


    /**
     * Ensures that the globals env-helpers file is registered in autoload.files
     * of the project's composer.json so that decrypt_env() is available before
     * Laravel's service container is booted (e.g. inside config/database.php).
     *
     * Runs composer dump-autoload when the entry was missing and has been added.
     *
     * @return      void
     *
     * @version     1.0.0 / 2026-04-26
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    private function _registerEnvHelpers(): void
    {
        $file     = 'vendor/bplan-base/globals/helpers/env-helpers.php';
        $Composer = ComposerJson::load(base_path('composer.json'));

        if ($Composer->hasAutoloadFile($file) === true) {
            return;
        }
        $this->_Output->caption('Register env helpers');
        $this->_Output->bulletMessage('Add env-helpers.php to autoload.files in composer.json');

        $Composer->addAutoloadFile($file)->save();

        $this->_Output->bulletMessage('Run `composer dump-autoload`');

        $this->_runShellCommand(['composer', 'dump-autoload']);

        $this->_done[] = 'env-helpers.php registered in autoload.files';

    } // _registerEnvHelpers()


    /**
     * Prints a summary of completed and pending steps at the end of the setup run.
     *
     * @return      void
     *
     * @version     1.0.0 / 2026-04-23
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    private function _printSummary(): void
    {
        $this->_Output->caption('Summary');

        if (!empty($this->_done)) {
            $this->_Output->print('Completed:', color: 'success', prependNewline: 0);

            foreach ($this->_done as $item) {
                $this->_Output->print($item, bullet: true, color: 'success');
            }
        }
        if (!empty($this->_pending)) {
            $this->_Output->print('Pending / manual steps:', color: 'highlight', prependNewline: 1);

            foreach ($this->_pending as $item) {
                $this->_Output->print($item, bullet: true, color: 'highlight');
            }
        }
    } // _printSummary()


    /**
     *
     * @return 	    void
     *
     * @version     1.2.0 / 2025-04-22
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    private function _registerPrivateGit(): void
    {
        if ($this->_installationMode !== 'local') {
            return;
        }
        if ($this->_hasPrivateGit === true) {
            return;
        }
        $this->_Output->caption('Register private Git Repository');
        $this->_Output->bulletMessage('Update composer.json with private Git repository');

        $this->_runShellCommand([
            'composer',
            'config',
            'repositories.'.$this->_privateGitName,
            'composer',
            $this->_privateGitUrl
        ]);
    } // _registerPrivateGit()


    /**
     * Builds the scheduler task name from the prefix and the APP_INSTANCE_ID in .env.
     *
     * Falls back to the bare prefix when no instance ID is set yet.
     *
     * @return      string
     *
     * @version     1.0.0 / 2026-04-25
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    private function _resolveSchedulerTaskName(): string
    {
        $instanceId = EnvFile::loadOrCreate(base_path('.env'))->get('APP_INSTANCE_ID');

        if ($instanceId !== '') {
            return self::SCHEDULER_TASK_PREFIX.' - '.$instanceId;
        }
        return self::SCHEDULER_TASK_PREFIX;

    } // _resolveSchedulerTaskName()


    /**
     * Returns true when a Windows Scheduled Task with the given name exists
     * and its "Start In" directory matches the current base_path().
     *
     * Used to distinguish tasks that belong to this installation from those
     * created by a copied installation sharing the same APP_INSTANCE_ID.
     *
     * @param       string $taskName
     *
     * @return      bool
     *
     * @version     1.0.0 / 2026-04-25
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    private function _schedulerTaskBelongsHere(string $taskName): bool
    {
        exec('schtasks /query /tn '.escapeshellarg($taskName).' /fo LIST /v 2>nul', $output, $exitCode);

        if ($exitCode !== 0) {
            return false;
        }
        foreach ($output as $line) {
            if (stripos($line, 'Start In:') === 0) {
                $taskPath    = trim(substr($line, strlen('Start In:')));
                $currentPath = rtrim(str_replace('\\', '/', base_path()), '/');
                $taskPath    = rtrim(str_replace('\\', '/', $taskPath), '/');

                return strcasecmp($taskPath, $currentPath) === 0;
            }
        }
        return false;

    } // _schedulerTaskBelongsHere()


    /**
     *
     * @return      void
     *
     * @version     1.1.0 / 2026-04-23
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    private function _runMigrations(): void
    {
        /*
        **  Production: manual step only — never run migrations automatically. */
        if ($this->_installationMode === 'production') {
            $this->_Output->caption('Database migrations');
            $this->_Output->bulletMessage('Run `php artisan migrate` manually after deployment.');

            $this->_pending[] = 'Run `php artisan migrate` manually';

            return;
        }

        $this->_Output->caption('Database migrations');

        /*
        **  Initial setup: always migrate:fresh --seed, no prompt. */
        $isInitial = $this->_installationState === self::STATE_FRESH
            || $this->_installationState === self::STATE_CHECKOUT;

        if ($isInitial === true) {
            $this->_runShellCommand([PHP_BINARY, base_path('artisan'), 'config:clear'], showOutput: true);
            $this->_runShellCommand([PHP_BINARY, base_path('artisan'), 'migrate:fresh', '--seed'], showOutput: true);

            $this->_done[] = 'Migrations: migrate:fresh --seed';

            return;
        }

        /*
        **  Subsequent run: let the user choose. Seeds only with migrate:fresh. */
        $choices = ['skip', 'migrate', 'migrate:fresh'];
        $choice  = (int) $this->_Prompt->choice('Run database migrations?', $choices, 0);

        if ($choice === 0) {
            $this->_pending[] = 'Run database migrations';

            return;
        }
        $this->_runShellCommand([PHP_BINARY, base_path('artisan'), 'config:clear'], showOutput: true);

        if ($choice === 1) {
            $this->_runShellCommand([PHP_BINARY, base_path('artisan'), 'migrate'], showOutput: true);

            $this->_done[] = 'Migrations: migrate';

        } else {
            $this->_runShellCommand([PHP_BINARY, base_path('artisan'), 'migrate:fresh', '--seed'], showOutput: true);

            $this->_done[] = 'Migrations: migrate:fresh --seed';
        }
    } // _runMigrations()




    /**
     *
     * @return      void
     *
     * @version     1.4.0 / 2026-04-25
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    private function _runProductionSetup(): void
    {
        /*
        **  Install dependencies without dev packages. */
        $this->_Output->caption('Install dependencies');
        $this->_Output->bulletMessage('Run `composer install --no-dev --optimize-autoloader`');

        $this->_runShellCommand([
            'composer',
            'install',
            '--no-dev',
            '--optimize-autoloader',
        ]);

        /*
        **  Build frontend assets. */
        $this->_Output->caption('Build frontend assets');
        $this->_Output->bulletMessage('Run `npm ci`');

        $this->_runShellCommand(['npm', 'ci']);

        $this->_Output->bulletMessage('Run `npm run build`');

        $this->_runShellCommand(['npm', 'run', 'build']);

        /*
        **  Optimize caches — opt-in only. Caching before all .env changes are final
        **  would lock in an incomplete configuration and cause hard-to-trace issues. */
        $this->_Output->caption('Optimize caches');

        if ($this->_Prompt->confirm('Build caches now? (Skip if .env changes are still pending)', false) === true) {
            $this->_Output->bulletMessage('package:discover');

            Artisan::call('package:discover');

            $this->_Output->bulletMessage('config:cache');

            Artisan::call('config:cache');

            $this->_Output->bulletMessage('event:cache');

            Artisan::call('event:cache');

            $this->_Output->bulletMessage('route:cache');

            Artisan::call('route:cache');

            $this->_Output->bulletMessage('view:cache');

            Artisan::call('view:cache');

            $this->_done[] = 'Caches built (config, event, route, view)';

        } else {
            $this->_Output->print('Caches were not built.', bullet: true);
            $this->_Output->print('Run `globals:setup` again once all configuration is finalised to build them.', bullet: true);

            $this->_pending[] = 'Build caches — run `globals:setup` again or: `artisan config:cache route:cache view:cache event:cache`';
        }

        /*
        **  Restart queue workers if not using the sync driver. */
        $queueConnection = EnvFile::load(base_path('.env'))->get('QUEUE_CONNECTION', 'sync');

        if ($queueConnection !== 'sync') {
            if ($this->_Prompt->confirm('Restart queue workers?', true) === true) {
                $this->_Output->caption('Queue');

                Artisan::call('queue:restart');

                $this->_Output->bulletMessage(trim(Artisan::output()) ?: 'Broadcasting restart signal.');
            }
        }
        $this->_setupScheduledTask();

    } // _runProductionSetup()




    /**
     *
     * @return      void
     *
     * @version     1.1.0 / 2026-04-20
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    private function _setInstallationMode(): void
    {
        switch ($this->_installationState) {
            case self::STATE_CHECKOUT:
                $modeIndex = (int) $this->_Prompt->choice(
                    'Select installation mode:',
                    ['local', 'dev-server', 'production', 'demo'],
                    0
                );
                $this->_installationMode = ['local', 'dev-server', 'production', 'demo'][$modeIndex];

                break;

            case self::STATE_CONFIGURED:
                $appEnv = EnvFile::loadOrCreate(base_path('.env'))->get('APP_ENV', 'local');

                $this->_installationMode = match ($appEnv) {
                    'production' => 'production',
                    'demo'       => 'demo',
                    default      => 'local',
                };

                break;

            case self::STATE_FRESH:
            default:
                $this->_installationMode = 'local';

                break;
        }
    } // _setInstallationMode()


    /**
     *
     * @return 	    void
     *
     * @version     1.1.0 / 2026-04-19
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    private function _setupApiPackage(): void
    {
        if ($this->_hasApi === true) {
            return;
        }
        if ($this->_firstRun === false) {
            return;
        }
        if ($this->_Prompt->confirm('Does the current project require an API?', true) === false) {
            return;
        }
        $this->_requireApi = true;

        $this->_Output->bulletMessage('Update composer.json with api-package ['.$this->_apiPackage.']');

        $this->_runShellCommand([
            'composer',
            'require',
            $this->_apiPackage,
            '--no-update', // '--no-install',
            // '--no-scripts',  '--no-autoloader',
        ]);
        $this->_installPackages++;

    } // _setupApiPackage()


    /**
     *
     * @return 	    void
     *
     * @version     2.0.0 / 2026-04-19
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    private function _setupCodeGenerator(): void
    {
        if ($this->_hasCodeGenerator === true) {
            return;
        }
        $choice = (int) $this->_Prompt->choice(
            'Do you want to install the code generator?',
            ['no', 'yes (local)', 'yes (from Git)'],
            0
        );

        if ($choice === 0) {
            return;
        }
        $ComposerLocal = ComposerJson::loadOrCreate(base_path('composer.local.json'));

        $constraint = '*';

        if ($choice === 1) {
            $localPath = $this->_Prompt->ask(
                'Please enter the full path to the local repository',
                validator: function ($input) {
                    return file_exists($input.'/src/GeneratorServiceProvider.php');
                }
            );
            $this->_Output->bulletMessage('Update composer.local.json with local repository');

            $ComposerLocal->addRepository(
                $this->_codeGeneratorPackage,
                'path',
                str_replace('\\', '/', $localPath)
            );
            $constraint = '@dev';
        }
        $this->_Output->bulletMessage('Update composer.local.json with code-generator ['.$this->_codeGeneratorPackage.']');

        $ComposerLocal->addPackage($this->_codeGeneratorPackage, $constraint, dev: true);
        $ComposerLocal->save();

        $this->_installPackages++;

    } // _setupCodeGenerator()


    /**
     *
     * @return 	    void
     *
     * @version     1.0.0 / 2025-06-22
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    private function _setupCodeGeneratorLoader(): void
    {
        if ($this->_hasCodeGeneratorLoader === true) {
            return;
        }
        $this->_Output->caption('Update composer.json with CodeGenerator Loader');

        $this->_Output->bulletMessage('Download package ['.$this->_codeGeneratorLoaderPackage.']');

        $this->_runShellCommand([
            'composer',
            'require',
            $this->_codeGeneratorLoaderPackage.':*',
            '--no-update', // '--no-install',
            // '--no-scripts',  '--no-autoloader',
        ]);
        $this->_installPackages++;

    } // _setupCodeGeneratorLoader()


    /**
     *
     * @return      void
     *
     * @version     1.0.0 / 2026-02-25
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    private function _setupHttpService(): void
    {
        if ($this->_hasHttpService === false) {
            if ($this->_Prompt->confirm('Does the current project need to access an external API?', true) === true) {
                $this->_Output->bulletMessage('Publishing "config/http-service.php"');

                Artisan::call('vendor:publish', [
                    '--provider' => \BplanBase\Globals\PackageServiceProvider::class,
                    '--tag' => 'http-service',
                ]);
                $this->_requireHttpService = true;
            }
        }
    } // _setupHttpService()


    /**
     *
     * @return 	    void
     *
     * @version     1.0.0 / 2026-02-28
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    private function _setupLangPackage(): void
    {
        if ($this->_hasLangPackage === true) {
            return;
        }
        $this->_Output->caption('Install language package');

        $this->_Output->bulletMessage('Download package [laravel-lang/lang]');

        $this->_runShellCommand([
            'composer',
            'require',
            'laravel-lang/lang',
            '--no-update', // '--no-install',
        ]);
        $this->_addLangUpdateCommand();

        $this->_installPackages++;

    } // _setupLangPackage()


    /**
     *
     * @return 	    void
     *
     * @version     1.3.0 / 2026-04-22
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    private function _setupMergePlugin(): void
    {
        if ($this->_hasMergePlugin === true) {
            return;
        }
        $this->_Output->caption('Update composer.json with merge-plugin ['.$this->_mergePlugin.']');

        $this->_Output->bulletMessage('Configure "allow-plugins".');

        $this->_runShellCommand([
            'composer',
            'config',
            'allow-plugins.'.$this->_mergePlugin,
            'true',
        ]);
        $this->_Output->bulletMessage('Configure "extra.merge-plugin.include"');

        $this->_runShellCommand([
            'composer',
            'config',
            'extra.merge-plugin.ignore-duplicates',
            'false',
        ]);
        $this->_runShellCommand([
            'composer',
            'config',
            'extra.merge-plugin.include',
            '--json',
            '["composer.local.json"]',
        ]);
        $this->_Output->bulletMessage('Configure "extra.merge-plugin.merge-dev"');

        $this->_runShellCommand([
            'composer',
            'config',
            'extra.merge-plugin.replace',
            'true',
        ]);

        $this->_runShellCommand([
            'composer',
            'config',
            'extra.merge-plugin.ignore-duplicates',
            'false',
        ]);
        $this->_Output->bulletMessage('Download package ['.$this->_mergePlugin.']');

        $this->_runShellCommand([
            'composer',
            'require',
            $this->_mergePlugin,
            '--dev',
             '--no-update', // '--no-install',
        ]);
        $this->_installPackages++;

    } // _setupMergePlugin()


    /**
     * Installs node_modules when the directory does not yet exist.
     *
     * Uses `npm ci` when a package-lock.json is present (reproducible install),
     * otherwise falls back to `npm install`.
     *
     * Skipped entirely when node_modules/ already exists.
     *
     * @return      void
     *
     * @version     1.0.0 / 2026-04-23
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    private function _setupNodeModules(): void
    {
        if (is_dir(base_path('node_modules')) === true) {
            return;
        }
        $this->_Output->caption('Install node modules');

        if (file_exists(base_path('package-lock.json')) === true) {
            $this->_Output->bulletMessage('Run `npm ci`');

            $this->_runShellCommand(['npm', 'ci']);
        } else {
            $this->_Output->bulletMessage('Run `npm install`');

            $this->_runShellCommand(['npm', 'install']);
        }
        $this->_done[] = 'Node modules installed';

    } // _setupNodeModules()


    /**
     * Creates a Windows Scheduled Task that runs the Laravel Scheduler every minute.
     *
     * Skipped when the task already belongs to this installation.
     * When a task with the same name exists but points to a different directory
     * (copied installation), a new APP_INSTANCE_ID is generated first so the
     * new task gets a unique name.
     *
     * Always runs as SYSTEM — no password required.
     *
     * @return      void
     *
     * @version     1.0.0 / 2026-04-25
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    private function _setupScheduledTask(): void
    {
        $taskName = $this->_resolveSchedulerTaskName();

        if ($this->_schedulerTaskBelongsHere($taskName) === true) {
            $this->_Output->bulletMessage('Scheduled task already exists: '.$taskName);

            return;
        }
        if ($this->_wantsScheduledTask === false) {
            $this->_pending[] = 'Set up scheduler: Windows Task Scheduler → every 1 minute → `php artisan schedule:run`';

            return;
        }

        /*
        **  A task with the same name exists but belongs to a different installation
        **  (e.g. a copied .env). Generate a fresh instance ID so names do not collide. */
        exec('schtasks /query /tn '.escapeshellarg($taskName).' /fo LIST 2>nul', result_code: $existsCode);

        if ($existsCode === 0) {
            $id = substr(str_replace('-', '', (string) \Illuminate\Support\Str::uuid()), 0, 12);

            EnvFile::load(base_path('.env'))->set('APP_INSTANCE_ID', $id)->save();

            $taskName = $this->_resolveSchedulerTaskName();
        }
        $phpBinary  = PHP_BINARY;
        $artisan    = base_path('artisan');
        $workingDir = base_path();
        $taskRun    = '"'.$phpBinary.'" "'.$artisan.'" schedule:run >> NUL 2>&1';

        $this->_Output->caption('Windows Scheduled Task');
        $this->_Output->bulletMessage('Creating task: '.$taskName);

        $this->_runShellCommand([
            'schtasks', '/create',
            '/tn', $taskName,
            '/tr', $taskRun,
            '/sc', 'MINUTE',
            '/mo', '1',
            '/ru', 'SYSTEM',
            '/sd', $workingDir,
            '/f',
        ]);
        $this->_done[] = 'Scheduled task created: '.$taskName;

    } // _setupScheduledTask()


    /**
     * Registers the globals package Blade view paths in config/tailwind-content.json.
     *
     * Does nothing when the file does not exist or the path is already present.
     *
     * @return      void
     *
     * @version     1.0.0 / 2026-04-22
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    private function _setupTailwindContent(): void
    {
        $contentFile = base_path('config/tailwind-content.json');

        if (file_exists($contentFile) === false) {
            return;
        }
        $globalsPath = './vendor/bplan-base/globals/resources/views/**/*.blade.php';

        $File = JsonFile::load($contentFile);

        if ($File->hasInArray('content', $globalsPath) === false) {
            $this->_Output->bulletMessage('Register globals views in tailwind-content.json');

            $File->addToArray('content', $globalsPath)->save();
        }
    } // _setupTailwindContent()


    /**
     *
     * @return      int
     *
     * @version     2.9.0 / 2026-04-25
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    public function handle(): int
    {
        $this->_init();

        if ($this->_installationState === self::STATE_INVALID) {
            return Command::FAILURE;
        }
        clear();

        $this->_Output->title('Globals Setup');

        $this->_setInstallationMode();

        if ($this->_checkSchedulerAdminRights() === false) {
            return Command::FAILURE;
        }
        $this->_Output->newline();
        $this->_Output->bulletMessage('Mode: '.$this->_installationMode.' | State: '.$this->_installationState);

        if ($this->_installationMode === 'local') {
            $this->_registerPrivateGit();
            $this->_setupApiPackage();
            $this->_setupHttpService();
            $this->_setupCodeGenerator();
            $this->_setupMergePlugin();
            $this->_setupLangPackage();
        }
        if ($this->_isDevMode() === true) {
            $this->_setupCodeGeneratorLoader();
            $this->_prepareEnvFiles();
            $this->_finishPackageSetup();
        }

        $this->_linkStorage();
        $this->_publishFiles();
        $this->_setupTailwindContent();

        if ($this->_isDevMode() === true) {
            $this->_setupNodeModules();
        }
        if ($this->_isProdMode() === true) {
            $this->_prepareEnvFiles();
        }
        $this->_registerEnvHelpers();
        $this->_configureApp();
        $this->_configureTimezone();
        $this->_configureDatabase();
        $this->_configureMail();

        $this->_runMigrations();

        if ($this->_isProdMode() === true) {
            $this->_runProductionSetup();
        }
        if ($this->_isDevMode() === true && $this->option('with-schedule') === true) {
            $this->_setupScheduledTask();
        }
        $this->_printSummary();

        return Command::SUCCESS;

    } // handle()


/* +++ CLASS METHODS +++ ++++++++++++++++++++++++++++++++++++++++++++++++++++ */


} // class GlobalsSetup extends Command {}
