<?php
declare(strict_types=1);
// src/controllers/AdminController.php

require_once __DIR__ . '/../../config/config.php';
require_once PROJECT_ROOT . '/src/models/AdminModel.php';

class AdminController
{

            /** Enforce authentication (401). */
            public static function requireAuth(): void
            {
                if (empty($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
                    http_response_code(401);
                    header('Content-Type: application/json');
                    echo json_encode(['error' => 'Unauthorized']);
                    exit;
                }
            }
        
            /** Enforce admin (401). */
            public static function requireAdmin(): void
        {
            self::requireAuth();
        
            // Prefer the session flag
            $isAdmin = (!empty($_SESSION['isAdmin']) && $_SESSION['isAdmin'] === true);
        
            // Fallback: check the user’s role in storage (e.g., users.txt/DB)
            if (!$isAdmin) {
                $u = $_SESSION['username'] ?? '';
                if ($u) {
                    try {
                        // UserModel::getUserRole($u) should return '1' for admins
                        $isAdmin = (UserModel::getUserRole($u) === '1');
                        if ($isAdmin) {
                            // Normalize session so downstream ACL checks see admin
                            $_SESSION['isAdmin'] = true;
                        }
                    } catch (\Throwable $e) {
                        // ignore and continue to deny
                    }
                }
            }
        
            if (!$isAdmin) {
                http_response_code(403);
                header('Content-Type: application/json');
                echo json_encode(['error' => 'Admin privileges required.']);
                exit;
            }
        }
            /** Get headers in lowercase, robust across SAPIs. */
            private static function headersLower(): array
            {
                $headers = function_exists('getallheaders') ? getallheaders() : [];
                $out = [];
                foreach ($headers as $k => $v) {
                    $out[strtolower($k)] = $v;
                }
                // Fallbacks from $_SERVER if needed
                foreach ($_SERVER as $k => $v) {
                    if (strpos($k, 'HTTP_') === 0) {
                        $h = strtolower(str_replace('_', '-', substr($k, 5)));
                        if (!isset($out[$h])) $out[$h] = $v;
                    }
                }
                return $out;
            }
        
            /** Enforce CSRF using X-CSRF-Token header (or csrfToken param as fallback). */
            public static function requireCsrf(): void
            {
                $h = self::headersLower();
                $token = trim($h['x-csrf-token'] ?? ($_POST['csrfToken'] ?? ''));
                if (empty($_SESSION['csrf_token']) || $token !== $_SESSION['csrf_token']) {
                    http_response_code(403);
                    header('Content-Type: application/json');
                    echo json_encode(['error' => 'Invalid CSRF token']);
                    exit;
                }
            }
        
            /** Read JSON body (empty array if not valid). */
            private static function readJson(): array
            {
                $raw = file_get_contents('php://input');
                $data = json_decode($raw, true);
                return is_array($data) ? $data : [];
            }

            public function getConfig(): void
            {
                header('Content-Type: application/json; charset=utf-8');
            
                $config = AdminModel::getConfig();
                if (isset($config['error'])) {
                    http_response_code(500);
                    header('Cache-Control: no-store');
                    echo json_encode(['error' => $config['error']], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
                    return;
                }
            
                // ---- Effective ONLYOFFICE values (constants override adminConfig) ----
                $ooCfg      = is_array($config['onlyoffice'] ?? null) ? $config['onlyoffice'] : [];
                $effEnabled = defined('ONLYOFFICE_ENABLED')
                    ? (bool) ONLYOFFICE_ENABLED
                    : (bool) ($ooCfg['enabled'] ?? false);
            
                $effDocs = (defined('ONLYOFFICE_DOCS_ORIGIN') && ONLYOFFICE_DOCS_ORIGIN !== '')
                    ? (string) ONLYOFFICE_DOCS_ORIGIN
                    : (string) ($ooCfg['docsOrigin'] ?? '');
            
                $hasSecret = defined('ONLYOFFICE_JWT_SECRET')
                    ? (ONLYOFFICE_JWT_SECRET !== '')
                    : (!empty($ooCfg['jwtSecret']));
            
                $publicOriginCfg = (string) ($ooCfg['publicOrigin'] ?? '');
            
                // ---- Pro / license info (all guarded for clean core installs) ----
                $licenseString = null;
                if (defined('PRO_LICENSE_FILE') && PRO_LICENSE_FILE && @is_file(PRO_LICENSE_FILE)) {
                    $json = @file_get_contents(PRO_LICENSE_FILE);
                    if ($json !== false) {
                        $decoded = json_decode($json, true);
                        if (is_array($decoded) && !empty($decoded['license'])) {
                            $licenseString = (string) $decoded['license'];
                        }
                    }
                }
            
                $proActive = defined('FR_PRO_ACTIVE') && FR_PRO_ACTIVE;
            
                // FR_PRO_INFO is only defined when bootstrap_pro.php has run; guard it
                $proPayload = [];
                if (defined('FR_PRO_INFO') && is_array(FR_PRO_INFO)) {
                    $p = FR_PRO_INFO['payload'] ?? null;
                    if (is_array($p)) {
                        $proPayload = $p;
                    }
                }
            
                $proType    = $proPayload['type']  ?? null;
                $proEmail   = $proPayload['email'] ?? null;
                $proVersion = defined('FR_PRO_BUNDLE_VERSION') ? FR_PRO_BUNDLE_VERSION : null;
            
                // Whitelisted public subset only (+ ONLYOFFICE enabled flag)
                $public = [
                    'header_title'        => (string)($config['header_title'] ?? 'FileRise'),
                    'loginOptions'        => [
                        'disableFormLogin' => (bool)($config['loginOptions']['disableFormLogin'] ?? false),
                        'disableBasicAuth' => (bool)($config['loginOptions']['disableBasicAuth'] ?? false),
                        'disableOIDCLogin' => (bool)($config['loginOptions']['disableOIDCLogin'] ?? false),
                    ],
                    'globalOtpauthUrl'    => (string)($config['globalOtpauthUrl'] ?? ''),
                    'enableWebDAV'        => (bool)($config['enableWebDAV'] ?? false),
                    'sharedMaxUploadSize' => (int)($config['sharedMaxUploadSize'] ?? 0),
                    'oidc' => [
                        'providerUrl' => (string)($config['oidc']['providerUrl'] ?? ''),
                        'redirectUri' => (string)($config['oidc']['redirectUri'] ?? ''),
                        // never include clientId/clientSecret
                    ],
                    'onlyoffice' => [
                        // Public only needs to know if it’s on; no secrets/origins here.
                        'enabled' => $effEnabled,
                    ],
                    'branding' => [
                        'customLogoUrl' => (string)($config['branding']['customLogoUrl'] ?? ''),
                        'headerBgLight' => (string)($config['branding']['headerBgLight'] ?? ''),
                        'headerBgDark'  => (string)($config['branding']['headerBgDark'] ?? ''),
                    ],
                    'pro' => [
                        'active'  => $proActive,
                        'type'    => $proType,
                        'email'   => $proEmail,
                        'version' => $proVersion,
                        'license' => $licenseString,
                    ],
                    'demoMode' => defined('FR_DEMO_MODE') ? (bool)FR_DEMO_MODE : false,
                ];
            
                $isAdmin = !empty($_SESSION['authenticated']) && !empty($_SESSION['isAdmin']);
            
                if ($isAdmin) {
                    // admin-only extras: presence flags + proxy options + ONLYOFFICE effective view
                    $adminExtra = [
                        'loginOptions' => array_merge($public['loginOptions'], [
                            'authBypass'     => (bool)($config['loginOptions']['authBypass'] ?? false),
                            'authHeaderName' => (string)($config['loginOptions']['authHeaderName'] ?? 'X-Remote-User'),
                        ]),
                        'oidc' => array_merge($public['oidc'], [
                            'hasClientId'     => !empty($config['oidc']['clientId']),
                            'hasClientSecret' => !empty($config['oidc']['clientSecret']),
                        ]),
                        'onlyoffice' => [
                            'enabled'      => $effEnabled,
                            'docsOrigin'   => $effDocs,         // effective (constants win)
                            'publicOrigin' => $publicOriginCfg, // optional override from adminConfig
                            'hasJwtSecret' => (bool)$hasSecret, // boolean only; never leak secret
                            'lockedByPhp'  => (
                                defined('ONLYOFFICE_ENABLED')
                                || defined('ONLYOFFICE_DOCS_ORIGIN')
                                || defined('ONLYOFFICE_JWT_SECRET')
                                || defined('ONLYOFFICE_PUBLIC_ORIGIN')
                            ),
                        ],
                    ];
            
                    header('Cache-Control: no-store'); // don’t cache admin config
                    echo json_encode(array_merge($public, $adminExtra), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
                    return;
                }
            
                // Non-admins / unauthenticated: only the public subset
                header('Cache-Control: no-store');
                echo json_encode($public, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
                return;
            }

public function setLicense(): void
{
    // Always respond JSON
    header('Content-Type: application/json; charset=utf-8');

    try {
        // Same guards as other admin endpoints
        self::requireAuth();
        self::requireAdmin();
        self::requireCsrf();

        $raw = file_get_contents('php://input');
        $data = json_decode($raw ?: '{}', true);
        if (!is_array($data)) {
            http_response_code(400);
            echo json_encode(['success' => false, 'error' => 'Invalid JSON body']);
            return;
        }

        $license = isset($data['license']) ? trim((string)$data['license']) : '';

        // Store license + updatedAt in JSON file
        if (!defined('PRO_LICENSE_FILE')) {
            // Fallback if constant not defined for some reason
            define('PRO_LICENSE_FILE', rtrim(USERS_DIR, "/\\") . '/proLicense.json');
        }

        $payload = [
            'license'   => $license,
            'updatedAt' => gmdate('c'),
        ];

        $dir = dirname(PRO_LICENSE_FILE);
        if (!is_dir($dir) && !mkdir($dir, 0755, true)) {
            http_response_code(500);
            echo json_encode(['success' => false, 'error' => 'Failed to create license dir']);
            return;
        }

        $json = json_encode($payload, JSON_PRETTY_PRINT);
        if ($json === false || file_put_contents(PRO_LICENSE_FILE, $json) === false) {
            http_response_code(500);
            echo json_encode(['success' => false, 'error' => 'Failed to write license file']);
            return;
        }

        echo json_encode(['success' => true]);
    } catch (Throwable $e) {
        http_response_code(500);
        echo json_encode([
            'success' => false,
            'error'   => 'Exception: ' . $e->getMessage(),
        ]);
    }
}

public function getProPortals(): array
{
    if (!defined('FR_PRO_ACTIVE') || !FR_PRO_ACTIVE || !defined('FR_PRO_BUNDLE_DIR') || !FR_PRO_BUNDLE_DIR) {
        throw new RuntimeException('FileRise Pro is not active.');
    }

    $proPortalsPath = rtrim((string)FR_PRO_BUNDLE_DIR, "/\\") . '/ProPortals.php';
    if (!is_file($proPortalsPath)) {
        throw new RuntimeException('ProPortals.php not found in Pro bundle.');
    }

    require_once $proPortalsPath;

    // ProPortals is implemented in the Pro bundle and handles JSON storage.
    $store   = new ProPortals(FR_PRO_BUNDLE_DIR);
    $portals = $store->listPortals();

    return $portals;
}

/**
 * @param array $portalsPayload Raw "portals" array from JSON body
 */
public function saveProPortals(array $portalsPayload): void
{
    if (!defined('FR_PRO_ACTIVE') || !FR_PRO_ACTIVE || !defined('FR_PRO_BUNDLE_DIR') || !FR_PRO_BUNDLE_DIR) {
        throw new RuntimeException('FileRise Pro is not active.');
    }

    $proPortalsPath = rtrim((string)FR_PRO_BUNDLE_DIR, "/\\") . '/ProPortals.php';
    if (!is_file($proPortalsPath)) {
        throw new RuntimeException('ProPortals.php not found in Pro bundle.');
    }

    require_once $proPortalsPath;

    if (!is_array($portalsPayload)) {
        throw new InvalidArgumentException('Invalid portals format.');
    }

    // Minimal normalization; deeper validation can live inside ProPortals
    $data = ['portals' => []];

    foreach ($portalsPayload as $slug => $info) {
        $slug = trim((string)$slug);
        if ($slug === '') {
            continue;
        }
        if (!is_array($info)) {
            $info = [];
        }

        $label        = trim((string)($info['label'] ?? $slug));
        $folder       = trim((string)($info['folder'] ?? ''));
        $clientEmail  = trim((string)($info['clientEmail'] ?? ''));
        $uploadOnly   = !empty($info['uploadOnly']);
        $allowDownload = array_key_exists('allowDownload', $info)
            ? !empty($info['allowDownload'])
            : true;
        $expiresAt    = trim((string)($info['expiresAt'] ?? ''));
    
        // Optional branding + form behavior
        $title      = trim((string)($info['title'] ?? ''));
        $introText  = trim((string)($info['introText'] ?? ''));
        $requireForm = !empty($info['requireForm']);
        $brandColor = trim((string)($info['brandColor'] ?? ''));
    $footerText = trim((string)($info['footerText'] ?? ''));

    $formDefaults = isset($info['formDefaults']) && is_array($info['formDefaults'])
        ? $info['formDefaults']
        : [];

    // Normalize defaults for known keys
    $formDefaults = [
        'name'      => trim((string)($formDefaults['name'] ?? '')),
        'email'     => trim((string)($formDefaults['email'] ?? '')),
        'reference' => trim((string)($formDefaults['reference'] ?? '')),
        'notes'     => trim((string)($formDefaults['notes'] ?? '')),
    ];
    $formRequired = isset($info['formRequired']) && is_array($info['formRequired'])
    ? $info['formRequired']
    : [];

    $formRequired = [
    'name'      => !empty($formRequired['name']),
    'email'     => !empty($formRequired['email']),
    'reference' => !empty($formRequired['reference']),
    'notes'     => !empty($formRequired['notes']),
];
    
        if ($folder === '') {
            continue;
        }
    
        $data['portals'][$slug] = [
            'label'         => $label,
            'folder'        => $folder,
            'clientEmail'   => $clientEmail,
            'uploadOnly'    => $uploadOnly,
            'allowDownload' => $allowDownload,
            'expiresAt'     => $expiresAt,
            // NEW
            'title'         => $title,
            'introText'     => $introText,
            'requireForm'   => $requireForm,
            'brandColor'    => $brandColor,
            'footerText'    => $footerText,
            'formDefaults'  => $formDefaults,
            'formRequired'  => $formRequired,
        ];
    }

    $store = new ProPortals(FR_PRO_BUNDLE_DIR);
    $ok    = $store->savePortals($data);

    if (!$ok) {
        throw new RuntimeException('Could not write portals.json');
    }
}

public function getProGroups(): array
{
    if (!defined('FR_PRO_ACTIVE') || !FR_PRO_ACTIVE || !defined('FR_PRO_BUNDLE_DIR') || !FR_PRO_BUNDLE_DIR) {
        throw new RuntimeException('FileRise Pro is not active.');
    }

    $proGroupsPath = rtrim((string)FR_PRO_BUNDLE_DIR, "/\\") . '/ProGroups.php';
    if (!is_file($proGroupsPath)) {
        throw new RuntimeException('ProGroups.php not found in Pro bundle.');
    }

    require_once $proGroupsPath;

    $store  = new ProGroups(FR_PRO_BUNDLE_DIR);
    $groups = $store->listGroups();

    return $groups;
}

/**
 * @param array $groupsPayload Raw "groups" array from JSON body
 */
public function saveProGroups(array $groupsPayload): void
{
    if (!defined('FR_PRO_ACTIVE') || !FR_PRO_ACTIVE || !defined('FR_PRO_BUNDLE_DIR') || !FR_PRO_BUNDLE_DIR) {
        throw new RuntimeException('FileRise Pro is not active.');
    }

    $proGroupsPath = rtrim((string)FR_PRO_BUNDLE_DIR, "/\\") . '/ProGroups.php';
    if (!is_file($proGroupsPath)) {
        throw new RuntimeException('ProGroups.php not found in Pro bundle.');
    }

    require_once $proGroupsPath;

    // Normalize / validate the payload into the canonical structure
    if (!is_array($groupsPayload)) {
        throw new InvalidArgumentException('Invalid groups format.');
    }

    $data = ['groups' => []];

    foreach ($groupsPayload as $name => $info) {
        $name = trim((string)$name);
        if ($name === '') {
            continue;
        }

        $label   = isset($info['label']) ? trim((string)$info['label']) : $name;
        $members = isset($info['members']) && is_array($info['members']) ? $info['members'] : [];
        $grants  = isset($info['grants']) && is_array($info['grants']) ? $info['grants'] : [];

        $data['groups'][$name] = [
            'name'    => $name,
            'label'   => $label,
            'members' => array_values(array_unique(array_map('strval', $members))),
            'grants'  => $grants,
        ];
    }

    $store = new ProGroups(FR_PRO_BUNDLE_DIR);
    if (!$store->save($data)) {
        throw new RuntimeException('Could not write groups.json');
    }
}

public function installProBundle(): void
{
    header('Content-Type: application/json; charset=utf-8');

    try {
        // Guard rails: method + auth + CSRF
        if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') {
            http_response_code(405);
            echo json_encode(['success' => false, 'error' => 'Method not allowed']);
            return;
        }

        self::requireAuth();
        self::requireAdmin();
        self::requireCsrf();

        // Ensure ZipArchive is available
        if (!class_exists('\\ZipArchive')) {
            http_response_code(500);
            echo json_encode(['success' => false, 'error' => 'ZipArchive extension is required on the server.']);
            return;
        }

        // Basic upload validation
        if (empty($_FILES['bundle']) || !is_array($_FILES['bundle'])) {
            http_response_code(400);
            echo json_encode(['success' => false, 'error' => 'Missing uploaded bundle (field "bundle").']);
            return;
        }

        $f = $_FILES['bundle'];

        if (!empty($f['error']) && $f['error'] !== UPLOAD_ERR_OK) {
            $msg = 'Upload error.';
            switch ($f['error']) {
                case UPLOAD_ERR_INI_SIZE:
                case UPLOAD_ERR_FORM_SIZE:
                    $msg = 'Uploaded file exceeds size limit.';
                    break;
                case UPLOAD_ERR_PARTIAL:
                    $msg = 'Uploaded file was only partially received.';
                    break;
                case UPLOAD_ERR_NO_FILE:
                    $msg = 'No file was uploaded.';
                    break;
                default:
                    $msg = 'Upload failed with error code ' . (int)$f['error'];
                    break;
            }
            http_response_code(400);
            echo json_encode(['success' => false, 'error' => $msg]);
            return;
        }

        $tmpName = $f['tmp_name'] ?? '';
        if ($tmpName === '' || !is_uploaded_file($tmpName)) {
            http_response_code(400);
            echo json_encode(['success' => false, 'error' => 'Invalid uploaded file.']);
            return;
        }

        // Guard against unexpectedly large bundles (e.g., >100MB)
        $size = isset($f['size']) ? (int)$f['size'] : 0;
        if ($size <= 0 || $size > 100 * 1024 * 1024) {
            http_response_code(413);
            echo json_encode(['success' => false, 'error' => 'Bundle size is invalid or too large (max 100MB).']);
            return;
        }

        // Optional: require .zip extension by name (best-effort)
        $origName = (string)($f['name'] ?? '');
        if ($origName !== '' && !preg_match('/\.zip$/i', $origName)) {
            http_response_code(400);
            echo json_encode(['success' => false, 'error' => 'Bundle must be a .zip file.']);
            return;
        }

        // Prepare temp working dir
        $tempRoot = rtrim(sys_get_temp_dir(), DIRECTORY_SEPARATOR);
        $workDir  = $tempRoot . DIRECTORY_SEPARATOR . 'filerise_pro_' . bin2hex(random_bytes(8));
        if (!@mkdir($workDir, 0700, true)) {
            http_response_code(500);
            echo json_encode(['success' => false, 'error' => 'Failed to prepare temp dir.']);
            return;
        }

        $zipPath = $workDir . DIRECTORY_SEPARATOR . 'bundle.zip';
        if (!@move_uploaded_file($tmpName, $zipPath)) {
            http_response_code(500);
            echo json_encode(['success' => false, 'error' => 'Failed to move uploaded bundle.']);
            return;
        }

        $zip = new \ZipArchive();
        if ($zip->open($zipPath) !== true) {
            http_response_code(400);
            echo json_encode(['success' => false, 'error' => 'Failed to open ZIP bundle.']);
            return;
        }

        $installed = [
            'src'    => [],
            'docs'   => [],
        ];

        $projectRoot = rtrim(PROJECT_ROOT, DIRECTORY_SEPARATOR);

        // Where Pro bundle code lives (defaults to USERS_DIR . '/pro')
        $projectRoot = rtrim(PROJECT_ROOT, DIRECTORY_SEPARATOR);
        $bundleRoot = defined('FR_PRO_BUNDLE_DIR')
            ? rtrim(FR_PRO_BUNDLE_DIR, DIRECTORY_SEPARATOR)
            : (rtrim(USERS_DIR, "/\\") . DIRECTORY_SEPARATOR . 'pro');

        // Put README-Pro.txt / LICENSE-Pro.txt inside the bundle dir as well
        $proDocsDir = $bundleRoot;
        if (!is_dir($proDocsDir)) {
            @mkdir($proDocsDir, 0755, true);
        }

        $allowedTopLevel = ['LICENSE-Pro.txt', 'README-Pro.txt'];

        // Iterate entries and selectively extract/copy expected files only
        for ($i = 0; $i < $zip->numFiles; $i++) {
            $name = $zip->getNameIndex($i);
            if ($name === false) {
                continue;
            }

            // Normalise and guard
            $name = ltrim($name, "/\\");
            if ($name === '' || substr($name, -1) === '/') {
                continue; // skip directories
            }
            if (strpos($name, '../') !== false || strpos($name, '..\\') !== false) {
                continue; // path traversal guard
            }

            // Ignore macOS Finder junk: __MACOSX and "._" resource forks
            $base = basename($name);
            if (
                str_starts_with($name, '__MACOSX/') ||
                str_contains($name, '/__MACOSX/') ||
                str_starts_with($base, '._')
            ) {
                continue;
            }

            $targetPath = null;
            $category   = null;

            if (in_array($name, $allowedTopLevel, true)) {
                // Docs → bundle dir (under /users/pro)
                $targetPath = $proDocsDir . DIRECTORY_SEPARATOR . $name;
                $category   = 'docs';

            } elseif (strpos($name, 'src/pro/') === 0) {
                // e.g. src/pro/bootstrap_pro.php -> FR_PRO_BUNDLE_DIR/bootstrap_pro.php
                $relative = substr($name, strlen('src/pro/'));
                if ($relative === '' || substr($relative, -1) === '/') {
                    continue;
                }
                $targetPath = $bundleRoot . DIRECTORY_SEPARATOR . $relative;
                $category   = 'src';

            } else {
                // Skip anything outside these prefixes
                continue;
            }

            if (!$targetPath || !$category) {
                continue;
            }

            // Track whether we're overwriting an existing file (for reporting only)
            $wasExisting = is_file($targetPath);

            // Read from ZIP entry
            $stream = $zip->getStream($name);
            if (!$stream) {
                continue;
            }

            $dir = dirname($targetPath);
            if (!is_dir($dir) && !@mkdir($dir, 0755, true)) {
                fclose($stream);
                http_response_code(500);
                echo json_encode(['success' => false, 'error' => 'Failed to create destination directory for ' . $name]);
                return;
            }

            $data = stream_get_contents($stream);
            fclose($stream);
            if ($data === false) {
                http_response_code(500);
                echo json_encode(['success' => false, 'error' => 'Failed to read data for ' . $name]);
                return;
            }

            // Always overwrite target file on install/upgrade
            if (@file_put_contents($targetPath, $data) === false) {
                http_response_code(500);
                echo json_encode(['success' => false, 'error' => 'Failed to write ' . $name]);
                return;
            }

            @chmod($targetPath, 0644);

            // Track what we installed (and whether it was overwritten)
            if (!isset($installed[$category])) {
                $installed[$category] = [];
            }
            $installed[$category][] = $targetPath . ($wasExisting ? ' (overwritten)' : '');
        }

        $zip->close();

        // Best-effort cleanup; ignore failures
        @unlink($zipPath);
        @rmdir($workDir);

        // Reflect current Pro status in response if bootstrap was loaded
        $proActive  = defined('FR_PRO_ACTIVE') && FR_PRO_ACTIVE;
        $proPayload = defined('FR_PRO_INFO') && is_array(FR_PRO_INFO)
            ? (FR_PRO_INFO['payload'] ?? null)
            : null;
        $proVersion = defined('FR_PRO_BUNDLE_VERSION') ? FR_PRO_BUNDLE_VERSION : null;

        echo json_encode([
            'success'    => true,
            'message'    => 'Pro bundle installed.',
            'installed'  => $installed,
            'proActive'  => (bool)$proActive,
            'proVersion' => $proVersion,
            'proPayload' => $proPayload,
        ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
    } catch (\Throwable $e) {
        http_response_code(500);
        echo json_encode([
            'success' => false,
            'error'   => 'Exception during bundle install: ' . $e->getMessage(),
        ]);
    }
}

    public function updateConfig(): void
    {
        header('Content-Type: application/json');

        // —– auth & CSRF checks —–
        if (
            !isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true ||
            !isset($_SESSION['isAdmin'])      || !$_SESSION['isAdmin']
        ) {
            http_response_code(403);
            echo json_encode(['error' => 'Unauthorized access.']);
            exit;
        }
        $headersArr    = array_change_key_case(getallheaders(), CASE_LOWER);
        $receivedToken = trim($headersArr['x-csrf-token'] ?? ($_POST['csrfToken'] ?? ''));
        if (!isset($_SESSION['csrf_token']) || $receivedToken !== $_SESSION['csrf_token']) {
            http_response_code(403);
            echo json_encode(['error' => 'Invalid CSRF token.']);
            exit;
        }

        // —– fetch payload —–
        $data = json_decode(file_get_contents('php://input'), true);
        if (!is_array($data)) {
            http_response_code(400);
            echo json_encode(['error' => 'Invalid input.']);
            exit;
        }

        // —– load existing on-disk config —–
        $existing = AdminModel::getConfig();
        if (isset($existing['error'])) {
            http_response_code(500);
            echo json_encode(['error' => $existing['error']]);
            exit;
        }

        // —– start merge with existing as base —–
        // Ensure minimal structure if the file was partially missing.
        $merged = $existing + [
            'header_title'        => '',
            'loginOptions'        => [
                'disableFormLogin' => false,
                'disableBasicAuth' => true,
                'disableOIDCLogin' => true,
                'authBypass'       => false,
                'authHeaderName'   => 'X-Remote-User'
            ],
            'globalOtpauthUrl'    => '',
            'enableWebDAV'        => false,
            'sharedMaxUploadSize' => 0,
            'oidc'                => [
                'providerUrl' => '',
                'clientId'    => '',
                'clientSecret'=> '',
                'redirectUri' => ''
            ],
            'branding'            => [
                'customLogoUrl' => '',
                'headerBgLight'   => '',
                'headerBgDark'    => '',
            ],
        ];

        // header_title (cap length and strip control chars)
        if (array_key_exists('header_title', $data)) {
            $title = trim((string)$data['header_title']);
            $title = preg_replace('/[\x00-\x1F\x7F]/', '', $title);
            if (mb_strlen($title) > 100) { // hard cap
                $title = mb_substr($title, 0, 100);
            }
            $merged['header_title'] = $title;
        }

        // loginOptions: inherit existing then override if provided
        foreach (['disableFormLogin','disableBasicAuth','disableOIDCLogin','authBypass'] as $flag) {
            if (isset($data['loginOptions'][$flag])) {
                $merged['loginOptions'][$flag] = filter_var(
                    $data['loginOptions'][$flag],
                    FILTER_VALIDATE_BOOLEAN
                );
            }
        }
        if (isset($data['loginOptions']['authHeaderName'])) {
            $hdr = trim((string)$data['loginOptions']['authHeaderName']);
            // very restrictive header-name pattern: letters, numbers, dashes
            if ($hdr !== '' && preg_match('/^[A-Za-z0-9\-]+$/', $hdr)) {
                $merged['loginOptions']['authHeaderName'] = $hdr;
            } else {
                http_response_code(400);
                echo json_encode(['error' => 'Invalid authHeaderName.']);
                exit;
            }
        }

        // globalOtpauthUrl
        if (array_key_exists('globalOtpauthUrl', $data)) {
            $merged['globalOtpauthUrl'] = trim((string)$data['globalOtpauthUrl']);
        }

        // enableWebDAV
        if (array_key_exists('enableWebDAV', $data)) {
            $merged['enableWebDAV'] = filter_var($data['enableWebDAV'], FILTER_VALIDATE_BOOLEAN);
        }

        // sharedMaxUploadSize
        if (array_key_exists('sharedMaxUploadSize', $data)) {
            $sms = filter_var($data['sharedMaxUploadSize'], FILTER_VALIDATE_INT);
            if ($sms === false || $sms < 0) {
                http_response_code(400);
                echo json_encode(['error' => 'sharedMaxUploadSize must be a non-negative integer (bytes).']);
                exit;
            }
            // Clamp to PHP limits to avoid confusing UX
            $maxPost  = self::iniToBytes(ini_get('post_max_size'));
            $maxFile  = self::iniToBytes(ini_get('upload_max_filesize'));
            $phpCap   = min($maxPost ?: PHP_INT_MAX, $maxFile ?: PHP_INT_MAX);
            if ($phpCap !== PHP_INT_MAX && $sms > $phpCap) {
                $sms = $phpCap;
            }
            $merged['sharedMaxUploadSize'] = $sms;
        }

        // oidc: only overwrite non-empty inputs; validate when enabling OIDC
        foreach (['providerUrl','clientId','clientSecret','redirectUri'] as $f) {
            if (!empty($data['oidc'][$f])) {
                $val = trim((string)$data['oidc'][$f]);
                if ($f === 'providerUrl' || $f === 'redirectUri') {
                    $val = filter_var($val, FILTER_SANITIZE_URL);
                }
                $merged['oidc'][$f] = $val;
            }
        }

        // If OIDC login is enabled, ensure required fields are present and sane
        $oidcEnabled = !empty($merged['loginOptions']['disableOIDCLogin']) ? false : true;
        if ($oidcEnabled) {
            $prov = $merged['oidc']['providerUrl'] ?? '';
            $rid  = $merged['oidc']['redirectUri'] ?? '';
            $cid  = $merged['oidc']['clientId'] ?? '';
            // clientSecret may be empty for some PKCE-only flows, but commonly needed for code flow.
            if ($prov === '' || $rid === '' || $cid === '') {
                http_response_code(400);
                echo json_encode(['error' => 'OIDC is enabled but providerUrl, redirectUri, and clientId are required.']);
                exit;
            }
            // Require https except for localhost development
            $httpsOk = function(string $url): bool {
                if ($url === '') return false;
                $parts = parse_url($url);
                if (!$parts || empty($parts['scheme'])) return false;
                if ($parts['scheme'] === 'https') return true;
                if ($parts['scheme'] === 'http' && (isset($parts['host']) && ($parts['host'] === 'localhost' || $parts['host'] === '127.0.0.1'))) {
                    return true;
                }
                return false;
            };
            if (!$httpsOk($prov) || !$httpsOk($rid)) {
                http_response_code(400);
                echo json_encode(['error' => 'providerUrl and redirectUri must be https (or http on localhost)']);
                exit;
            }
        }
        

        // —– persist merged config —–
                // ---- ONLYOFFICE: merge from payload (unless locked by PHP defines) ----
                $ooLockedByPhp = (
                    defined('ONLYOFFICE_ENABLED') ||
                    defined('ONLYOFFICE_DOCS_ORIGIN') ||
                    defined('ONLYOFFICE_JWT_SECRET') ||
                    defined('ONLYOFFICE_PUBLIC_ORIGIN')
                );
        
                if (!$ooLockedByPhp && isset($data['onlyoffice']) && is_array($data['onlyoffice'])) {
                    $ooExisting = (isset($existing['onlyoffice']) && is_array($existing['onlyoffice']))
                        ? $existing['onlyoffice'] : [];
        
                    $oo = $ooExisting;
        
                    if (array_key_exists('enabled', $data['onlyoffice'])) {
                        $oo['enabled'] = filter_var($data['onlyoffice']['enabled'], FILTER_VALIDATE_BOOLEAN);
                    }
                    if (isset($data['onlyoffice']['docsOrigin'])) {
                        $oo['docsOrigin'] = (string)$data['onlyoffice']['docsOrigin'];
                    }
                    if (isset($data['onlyoffice']['publicOrigin'])) {
                        $oo['publicOrigin'] = (string)$data['onlyoffice']['publicOrigin'];
                    }
                    // Allow setting/changing the secret when NOT locked by PHP
                    if (isset($data['onlyoffice']['jwtSecret'])) {
                        $js = trim((string)$data['onlyoffice']['jwtSecret']);
                        if ($js !== '') {
                            $oo['jwtSecret'] = $js; // stored encrypted by AdminModel
                        }
                        // If blank, we leave existing secret unchanged (no implicit wipe).
                    }
        
                    $merged['onlyoffice'] = $oo;
                }
                // Branding: pass through raw strings; AdminModel enforces Pro + sanitization.
            if (isset($data['branding']) && is_array($data['branding'])) {
            if (!isset($merged['branding']) || !is_array($merged['branding'])) {
                $merged['branding'] = [
                    'customLogoUrl'   => '',
                    'headerBgLight'   => '',
                    'headerBgDark'    => '',
                ];
            }
            foreach (['customLogoUrl', 'headerBgLight', 'headerBgDark'] as $key) {
                if (array_key_exists($key, $data['branding'])) {
                    $merged['branding'][$key] = (string)$data['branding'][$key];
                }
            }
        }

        $result = AdminModel::updateConfig($merged);
        if (isset($result['error'])) {
            http_response_code(500);
        }
        echo json_encode($result);
        exit;
    }

    /** Convert php.ini shorthand like "128M" to bytes */
    private static function iniToBytes($val)
    {
        if ($val === false || $val === null || $val === '') return 0;
        $val = trim((string)$val);
        $last = strtolower($val[strlen($val)-1]);
        $num = (int)$val;
        switch ($last) {
            case 'g': $num *= 1024;
            case 'm': $num *= 1024;
            case 'k': $num *= 1024;
        }
        return $num;
    }
}
?>