<?php

declare(strict_types=1);



class HeadlessInstaller
{
    

    public static function checkRequirements(): array
    {
        $minPhp = defined('TUBEPRESS_MIN_PHP') ? TUBEPRESS_MIN_PHP : '8.2.0';

        $required = [
            ['name' => 'php',              'label' => 'PHP >= ' . $minPhp,         'passed' => version_compare(PHP_VERSION, $minPhp, '>='),                 'current' => 'PHP ' . PHP_VERSION],
            ['name' => 'pdo_mysql',        'label' => 'pdo_mysql extension',       'passed' => extension_loaded('pdo') && extension_loaded('pdo_mysql'),    'current' => extension_loaded('pdo_mysql') ? 'loaded' : 'missing'],
            ['name' => 'mbstring',         'label' => 'mbstring extension',        'passed' => extension_loaded('mbstring'),                                'current' => extension_loaded('mbstring') ? 'loaded' : 'missing'],
            ['name' => 'fileinfo',         'label' => 'fileinfo extension',        'passed' => extension_loaded('fileinfo'),                                'current' => extension_loaded('fileinfo') ? 'loaded' : 'missing'],
            ['name' => 'config_writable',  'label' => 'config/ writable',          'passed' => self::dirWritable(APP_ROOT . '/config'),                     'current' => APP_ROOT . '/config'],
            ['name' => 'storage_writable', 'label' => 'storage/ writable',         'passed' => self::dirWritable(APP_ROOT . '/storage'),                    'current' => APP_ROOT . '/storage'],
            ['name' => 'uploads_writable', 'label' => 'public/uploads/ writable',  'passed' => self::dirWritable(APP_ROOT . '/public/uploads'),             'current' => APP_ROOT . '/public/uploads'],
        ];

        $recommended = [
            ['name' => 'curl',    'label' => 'curl extension (catalogue + updates)',        'passed' => extension_loaded('curl')],
            ['name' => 'zip',     'label' => 'zip extension (install plugins/themes)',      'passed' => extension_loaded('zip')],
            ['name' => 'gd',      'label' => 'gd or imagick extension (thumbnails)',        'passed' => extension_loaded('gd') || extension_loaded('imagick')],
            ['name' => 'exec',    'label' => 'exec() enabled (self-managed cron heartbeat)','passed' => self::execAvailable()],
        ];

        $ok = !in_array(false, array_column($required, 'passed'), true);

        return [
            'ok'          => $ok,
            'required'    => $required,
            'recommended' => $recommended,
            'php_version' => PHP_VERSION,
            'min_php'     => $minPhp,
        ];
    }

    

    public static function install(array $cfg): array
    {
        $steps = [];
        $note = static function (string $s) use (&$steps): void { $steps[] = $s; };

        $wasInstalled = App::isInstalled();
        if ($wasInstalled && empty($cfg['force'])) {
            return self::fail('already_installed', 'TubePress is already installed (config/installed.php exists). Pass force=true to re-run (non-destructive — it will not drop data).', $steps);
        }

         
        InstallController::ensureWritableDirs();
        $req = self::checkRequirements();
        if (!$req['ok']) {
            $missing = array_values(array_filter($req['required'], static fn($c) => !$c['passed']));
            return self::fail('requirements', 'Required environment checks failed: ' . implode(', ', array_column($missing, 'label')) . '.', $steps, ['requirements' => $req]);
        }
        $note('requirements: ok');

         
        $dbHostRaw = trim((string) ($cfg['db_host'] ?? '127.0.0.1'));
        if ($dbHostRaw === '') { $dbHostRaw = '127.0.0.1'; }
        $dbName = trim((string) ($cfg['db_name'] ?? ''));
        $dbUser = trim((string) ($cfg['db_user'] ?? ''));
        $dbPass = (string) ($cfg['db_pass'] ?? '');
        if ($dbName === '' || $dbUser === '') {
            return self::fail('input', 'db_name and db_user are required.', $steps);
        }
        $safeName = preg_replace('/[^A-Za-z0-9_-]/', '', $dbName);
        if ($safeName === '' || $safeName !== $dbName) {
            return self::fail('input', 'Invalid db_name "' . $dbName . '". Use only letters, numbers, underscore and hyphen.', $steps);
        }

        $adminUser = trim((string) ($cfg['admin_user'] ?? 'admin'));
        if (strlen($adminUser) < 3 || strlen($adminUser) > 50 || !preg_match('/^[A-Za-z0-9_-]+$/', $adminUser)) {
            return self::fail('input', 'admin_user must be 3-50 characters (letters, numbers, underscore, hyphen).', $steps);
        }
        $adminEmail = trim((string) ($cfg['admin_email'] ?? ''));
        if (!filter_var($adminEmail, FILTER_VALIDATE_EMAIL)) {
            return self::fail('input', 'admin_email is required and must be a valid email address.', $steps);
        }

         
         
        $generatedPassword = null;
        $adminPass = (string) ($cfg['admin_pass'] ?? '');
        if ($adminPass === '') {
            $adminPass = self::generatePassword();
            $generatedPassword = $adminPass;
        } elseif (strlen($adminPass) < 8) {
            return self::fail('input', 'admin_pass must be at least 8 characters.', $steps);
        }

        $siteName = trim((string) ($cfg['site_name'] ?? ''));
        if ($siteName === '') { $siteName = 'My Video Site'; }
        $siteDesc = trim((string) ($cfg['site_description'] ?? ''));
        $siteUrl  = rtrim(trim((string) ($cfg['site_url'] ?? '')), '/');

         
        [$h, $port, $socket] = InstallController::parseDbHost($dbHostRaw);
        $dsn = 'mysql:';
        if ($socket !== null) {
            $dsn .= 'unix_socket=' . $socket . ';';
        } else {
            $dsn .= 'host=' . $h . ';';
            if ($port !== null) { $dsn .= 'port=' . $port . ';'; }
        }
        $dsn .= 'charset=utf8mb4';

        try {
            $pdo = new PDO($dsn, $dbUser, $dbPass, [
                PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
                PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
                PDO::ATTR_EMULATE_PREPARES   => false,
            ]);
        } catch (PDOException $e) {
            return self::fail('database', 'Could not connect to MySQL: ' . self::dbHint($e), $steps);
        }

        try {
             
             
            try {
                $pdo->exec("USE `{$safeName}`");
            } catch (PDOException $eUse) {
                $pdo->exec("CREATE DATABASE IF NOT EXISTS `{$safeName}` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci");
                $pdo->exec("USE `{$safeName}`");
            }
             
            try { $pdo->exec("ALTER DATABASE `{$safeName}` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci"); } catch (\Throwable $eAlter) {   }
        } catch (PDOException $e) {
            return self::fail('database', "The database \"{$safeName}\" does not exist and this user cannot create it. Pre-create it and grant the user access, then retry.", $steps);
        }
        $note("database: connected ({$safeName})");

        require_once APP_ROOT . '/src/Install/Migrator.php';
        Database::set($pdo);
        $results = Migrator::run($pdo);
        $errors = array_values(array_filter($results, static fn($r) => ($r['status'] ?? '') === 'error'));
        if (!empty($errors)) {
            $files = array_column($errors, 'file');
            return self::fail('migrate', 'Database migrations failed (' . implode(', ', $files) . '). Ensure the DB user has CREATE, ALTER and INSERT privileges.', $steps, ['errors' => $errors]);
        }
        $applied = count(array_filter($results, static fn($r) => ($r['status'] ?? '') === 'success'));
        $note('migrations: ' . count($results) . ' files (' . $applied . ' newly applied, rest already present)');

         
        if ($socket !== null) {
            $runtimeDsn = 'mysql:unix_socket=' . $socket . ';dbname=' . $safeName . ';charset=utf8mb4';
        } else {
            $runtimeDsn = 'mysql:host=' . $h . ($port !== null ? ';port=' . $port : '') . ';dbname=' . $safeName . ';charset=utf8mb4';
        }
        $configContent = InstallController::buildDatabaseConfig($dbUser, $dbPass, $runtimeDsn);
        if (file_put_contents(APP_ROOT . '/config/database.php', $configContent) === false) {
            return self::fail('config', 'Could not write config/database.php — is the config/ directory writable?', $steps);
        }
         
        @chmod(APP_ROOT . '/config/database.php', 0640);
        $note('config/database.php: written');

         
        require_once APP_ROOT . '/config/database.php';
        $pdo = getDbConnection();
        Database::set($pdo);
        try {
            $hash = Auth::hashPassword($adminPass);
            $stmt = $pdo->prepare('INSERT INTO users (username, email, password_hash, role, is_active) VALUES (?, ?, ?, ?, 1)');
            $stmt->execute([$adminUser, $adminEmail, $hash, 'admin']);
            $note("admin: created {$adminUser} <{$adminEmail}>" . ($generatedPassword !== null ? ' (password auto-generated, returned below)' : ''));
        } catch (PDOException $e) {
            if (str_contains($e->getMessage(), 'Duplicate entry')) {
                 
                 
                $generatedPassword = null;  
                $note("admin: {$adminUser} already exists (kept as-is)");
            } else {
                return self::fail('admin', 'Could not create the admin account: ' . $e->getMessage(), $steps);
            }
        }

         
         
         
        Setting::set('site_name', $siteName);
        Setting::set('site_description', $siteDesc);
        if ($siteUrl !== '') {
            Setting::set('site_url', $siteUrl);
        }
        Setting::set('videos_per_page', '24');

        if (!$wasInstalled) {
            require_once APP_ROOT . '/src/Install/DefaultPages.php';
            DefaultPages::seed();
        }
        InstallController::seedDefaultSettings();
        SeoManager::boot();
        $note('settings, default pages and native SEO: seeded');

         
         
        foreach (['themes' => '../themes', 'templates' => '../templates'] as $link => $target) {
            $linkPath = APP_ROOT . '/public/' . $link;
            if (!file_exists($linkPath) && function_exists('symlink')) {
                @symlink($target, $linkPath);
            }
        }

         
        if (file_put_contents(APP_ROOT . '/config/installed.php', "<?php return true;\n") === false) {
            return self::fail('marker', 'Could not write config/installed.php — is the config/ directory writable?', $steps);
        }
        $note('installed marker: written');

        return [
            'ok'             => true,
            'version'        => defined('APP_VERSION') ? (string) APP_VERSION : '',
            'admin_user'     => $adminUser,
            'admin_email'    => $adminEmail,
            'admin_password' => $generatedPassword,  
            'site_name'      => $siteName,
            'site_url'       => $siteUrl,
            'database'       => $safeName,
            'steps'          => $steps,
        ];
    }

     

    private static function fail(string $code, string $message, array $steps, array $extra = []): array
    {
        return array_merge(['ok' => false, 'error' => $code, 'message' => $message, 'steps' => $steps], $extra);
    }

    private static function dirWritable(string $dir): bool
    {
        if (!is_dir($dir)) {
            @mkdir($dir, 0775, true);
        }
        return is_dir($dir) && is_writable($dir);
    }

    private static function execAvailable(): bool
    {
        if (!function_exists('exec')) {
            return false;
        }
        $disabled = array_map('trim', explode(',', strtolower((string) ini_get('disable_functions'))));
        return !in_array('exec', $disabled, true);
    }

    private static function dbHint(PDOException $e): string
    {
        $m = $e->getMessage();
        if (str_contains($m, 'Access denied')) {
            return 'access denied (wrong username or password).';
        }
        if (str_contains($m, 'Connection refused') || str_contains($m, 'No such file')) {
            return 'server unreachable (check the host/port/socket and that MySQL is running).';
        }
        return 'verify the credentials and that the MySQL server is running.';
    }

    

    private static function generatePassword(int $len = 24): string
    {
        $alphabet = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789';
        $max = strlen($alphabet) - 1;
        $out = '';
        for ($i = 0; $i < $len; $i++) {
            $out .= $alphabet[random_int(0, $max)];
        }
        return $out;
    }
}
