<?php

declare(strict_types=1);

namespace PpPhp\Core\Installer;

use PpPhp\Core\Installer\DatabaseManager;
use PpPhp\Core\Config;

class Controller
{
    private string $rootPath;
    private string $baseUrl;
    private string $assetsUrl;
    private string $appUrl;
    private string $step = '1';

    /**
     * @param string      $rootPath  the bundle root (where config/ is written)
     * @param string      $baseUrl   URL prefix of the installer entry point (no trailing slash)
     * @param string|null $assetsUrl override for where /assets lives — needed when the
     *                               document root IS the app's public/ folder, so assets
     *                               are at "{base}/assets" instead of "{base}/public/assets".
     * @param string|null $appUrl    override for the installed app's URL (post-install link).
     */
    public function __construct(string $rootPath, string $baseUrl, ?string $assetsUrl = null, ?string $appUrl = null)
    {
        $this->rootPath = $rootPath;
        $this->baseUrl = $baseUrl;
        $this->assetsUrl = $assetsUrl ?? ($baseUrl . '/public/assets');
        $this->appUrl = $appUrl ?? ($baseUrl . '/public/');

        if (session_status() === PHP_SESSION_NONE) {
            session_start();
        }
        $this->step = $_SESSION['install_step'] ?? '1';
        if (!in_array($this->step, ['1', '2'], true)) {
            $this->step = '1';
            $_SESSION['install_step'] = '1';
        }
    }

    public function run(): void
    {
        try {
            if ($_SERVER['REQUEST_METHOD'] === 'POST') {
                $this->handlePost();
            } else {
                $this->showStep();
            }
        } catch (\Throwable $exception) {
            error_log('pp-php installer error: ' . $exception->getMessage());
            $_SESSION['install_error'] = 'Installation could not continue: ' . $exception->getMessage();
            $this->redirectToStep(in_array($this->step, ['1', '2'], true) ? $this->step : '1');
        }
    }

    private function handlePost(): void
    {
        if (!$this->validCsrfToken((string)($_POST['_token'] ?? ''))) {
            $_SESSION['install_error'] = 'Your installer session expired. Please try again.';
            $this->redirectToStep($this->step);
            return;
        }

        if ($this->step === '2' && ($_POST['installer_action'] ?? '') === 'back') {
            $this->redirectToStep('1');
            return;
        }

        if ($this->step === '1') {
            $this->validateStep1();
        } elseif ($this->step === '2') {
            $this->validateStep2();
        }
    }

    private function showStep(): void
    {
        $stepFile = $this->rootPath . '/views/install/step' . $this->step . '_form.php';
        if (file_exists($stepFile)) {
            // Variables passed to view
            $baseUrl = $this->baseUrl;
            $assetsUrl = $this->assetsUrl;
            $appUrl = $this->appUrl;
            $installerAssetVersion = (string)max(
                (int)@filemtime($this->rootPath . '/public/assets/installer/installer.css'),
                (int)@filemtime($this->rootPath . '/public/assets/installer/installer-polish.css'),
                (int)@filemtime($this->rootPath . '/public/assets/installer/installer.js')
            );
            $csrfToken = $this->csrfToken();
            $currentStep = (int)$this->step;
            $dbSummary = is_array($_SESSION['db_config'] ?? null) ? $_SESSION['db_config'] : [];
            $error = $_SESSION['install_error'] ?? '';
            unset($_SESSION['install_error']);
            require $stepFile;
        } else {
            echo "Installation step not found.";
        }
    }

    private function validateStep1(): void
    {
        $dbHost = trim((string)($_POST['db_host'] ?? ''));
        $dbPort = trim((string)($_POST['db_port'] ?? '3306'));
        $dbName = trim((string)($_POST['db_name'] ?? ''));
        $dbUser = trim((string)($_POST['db_user'] ?? ''));
        $dbPass = (string)($_POST['db_pass'] ?? '');

        $_SESSION['install_db_old'] = [
            'host' => $dbHost,
            'port' => $dbPort,
            'dbname' => $dbName,
            'user' => $dbUser,
        ];

        if ($dbHost === '' || $dbName === '' || $dbUser === '' || !ctype_digit($dbPort) || (int)$dbPort < 1 || (int)$dbPort > 65535) {
            $_SESSION['install_error'] = 'Enter a valid host, port, database name, and database username.';
            $this->redirectToStep('1');
            return;
        }

        $dbManager = new DatabaseManager($dbHost, $dbPort, $dbName, $dbUser, $dbPass);

        if (!$dbManager->testConnection()) {
            $_SESSION['install_error'] = 'Could not connect to the database. Please check your credentials.';
            $this->redirectToStep('1');
            return;
        }

        // Connection successful, save temporarily in session for step 2
        $_SESSION['db_config'] = [
            'host' => $dbHost,
            'port' => $dbPort,
            'dbname' => $dbName,
            'user' => $dbUser,
            'pass' => $dbPass,
        ];
        unset($_SESSION['install_db_old']);

        $this->redirectToStep('2');
    }

    private function validateStep2(): void
    {
        $adminUser = trim((string)($_POST['admin_user'] ?? ''));
        $adminEmail = strtolower(trim((string)($_POST['admin_email'] ?? '')));
        $adminPass = (string)($_POST['admin_pass'] ?? '');
        $adminPassConfirm = (string)($_POST['admin_pass_confirm'] ?? '');

        $_SESSION['install_admin_old'] = [
            'username' => $adminUser,
            'email' => $adminEmail,
        ];

        if (!isset($_SESSION['db_config']) || !is_array($_SESSION['db_config'])) {
            $_SESSION['install_error'] = 'Database connection details expired. Please reconnect to continue.';
            $this->redirectToStep('1');
            return;
        }

        if (!preg_match('/^[a-zA-Z0-9_.-]{3,50}$/', $adminUser)) {
            $_SESSION['install_error'] = 'Admin username must be 3–50 characters and use only letters, numbers, dots, dashes, or underscores.';
            $this->redirectToStep('2');
            return;
        }

        if (!filter_var($adminEmail, FILTER_VALIDATE_EMAIL) || strlen($adminEmail) > 100) {
            $_SESSION['install_error'] = 'Please enter a valid admin email address.';
            $this->redirectToStep('2');
            return;
        }

        // The platform-owner identity is RESERVED — it is not created here. Owner
        // status is established separately via an email-verified challenge (see
        // OwnerVerification). Allowing it as the install admin was a privilege
        // escalation (the account would be auto-promoted to owner). Your first
        // administrator is a normal superadmin; pick a different name/email.
        if (strcasecmp($adminUser, \PpPhp\Core\Security::ownerUsername()) === 0
            || strcasecmp($adminEmail, \PpPhp\Core\Auth\OwnerVerification::ownerEmail()) === 0) {
            $_SESSION['install_error'] = 'That username or email is reserved for the platform owner and cannot be used for your administrator account. Please choose a different one.';
            $this->redirectToStep('2');
            return;
        }

        if ($adminPass !== $adminPassConfirm) {
            $_SESSION['install_error'] = 'Passwords do not match.';
            $this->redirectToStep('2');
            return;
        }

        if (strlen($adminPass) < 8) {
            $_SESSION['install_error'] = 'Password must be at least 8 characters.';
            $this->redirectToStep('2');
            return;
        }

        $dbConfig = $_SESSION['db_config'];
        $dbManager = new DatabaseManager(
            $dbConfig['host'],
            $dbConfig['port'],
            $dbConfig['dbname'],
            $dbConfig['user'],
            $dbConfig['pass']
        );

        // Create all required tables and seed admin user
        try {
            $dbManager->installTables($adminUser, $adminEmail, $adminPass);
        } catch (\Throwable $e) {
            error_log('pp-php installation failed: ' . $e->getMessage());
            $_SESSION['install_error'] = 'Installation failed: ' . $e->getMessage();
            $this->redirectToStep('2');
            return;
        }

        // Write database config file
        $configContent = "<?php\nreturn " . var_export($dbConfig, true) . ";\n";
        $configDir = $this->rootPath . '/config';
        if (!is_dir($configDir)) {
            mkdir($configDir, 0755, true);
        }
        $configPath = $configDir . '/database.php';
        $temporaryPath = $configDir . '/.database-' . bin2hex(random_bytes(6)) . '.tmp';
        if (file_put_contents($temporaryPath, $configContent, LOCK_EX) === false
            || !rename($temporaryPath, $configPath)) {
            @unlink($temporaryPath);
            $_SESSION['install_error'] = 'Installation reached the final step but database.php could not be written. Check config directory permissions and try again.';
            $this->redirectToStep('2');
            return;
        }
        @chmod($configPath, 0640);

        // On a plain server install (no pre-baked owner password), let the hidden
        // owner account adopt the password just chosen, so the vendor can sign in
        // to the Licence server without hunting for a random one. No-op on a
        // customer install whose owner password is baked in or arrives by lease.
        try {
            require_once $this->rootPath . '/core/Helpers.php';
            \PpPhp\Core\DB::Init();
            \PpPhp\Core\DB::SetConnection();
            \PpPhp\Core\Auth\OwnerCredentials::adoptInstallPasswordIfUnconfigured($adminPass);
        } catch (\Throwable $e) {
            // never let this optional convenience break a successful install
        }

        // Confirm what just happened, in place. This cannot be a redirect: once
        // config/database.php exists, install.php sends every later visit to the
        // app, so this render is the only chance to tell them what to do next.
        $isLicensed = false;
        try {
            $isLicensed = \PpPhp\Core\Licensing\LicenseTrust::isStamped();
        } catch (\Throwable $e) {
            // unstamped source, or licensing not shipped in this edition
        }

        unset($_SESSION['install_admin_old']);
        session_destroy();

        $baseUrl = $this->baseUrl;
        $assetsUrl = $this->assetsUrl;
        $appUrl = $this->appUrl;
        $installerAssetVersion = (string) max(
            (int) @filemtime($this->rootPath . '/public/assets/installer/installer.css'),
            (int) @filemtime($this->rootPath . '/public/assets/installer/installer.js')
        );
        $doneView = $this->rootPath . '/views/install/step_done.php';
        if (is_file($doneView)) {
            require $doneView;
            exit;
        }
        header('Location: ' . $this->appUrl);
        exit;
    }

    private function redirectToStep(string $step): void
    {
        $_SESSION['install_step'] = $step;
        header('Location: ' . $this->baseUrl . '/install.php');
        exit;
    }

    private function csrfToken(): string
    {
        if (empty($_SESSION['installer_csrf_token'])) {
            $_SESSION['installer_csrf_token'] = bin2hex(random_bytes(32));
        }
        return (string)$_SESSION['installer_csrf_token'];
    }

    private function validCsrfToken(string $token): bool
    {
        return $token !== ''
            && !empty($_SESSION['installer_csrf_token'])
            && hash_equals((string)$_SESSION['installer_csrf_token'], $token);
    }
}
