<?php
// src/models/FileModel.php

require_once PROJECT_ROOT . '/config/config.php';
require_once __DIR__ . '/../../src/lib/ACL.php';

class FileModel {

    /**
     * Resolve a logical folder key (e.g. "root", "invoices/2025") to a
     * real path under UPLOAD_DIR, enforce REGEX_FOLDER_NAME, and ensure
     * optional creation.
     *
     * @param string $folder
     * @param bool   $create
     * @return array [string|null $realPath, string|null $error]
     */
    private static function resolveFolderPath(string $folder, bool $create = true): array {
        $folder = trim($folder) ?: 'root';

        if (strtolower($folder) !== 'root' && !preg_match(REGEX_FOLDER_NAME, $folder)) {
            return [null, "Invalid folder name."];
        }

        $base = realpath(UPLOAD_DIR);
        if ($base === false) {
            return [null, "Server misconfiguration."];
        }

        $dir = (strtolower($folder) === 'root')
            ? $base
            : $base . DIRECTORY_SEPARATOR . trim($folder, "/\\ ");

        if ($create && !is_dir($dir) && !mkdir($dir, 0775, true)) {
            return [null, "Cannot create destination folder"];
        }

        $real = realpath($dir);
        if ($real === false || strpos($real, $base) !== 0) {
            return [null, "Invalid folder path."];
        }
        return [$real, null];
    }

    /**
     * Copies files from a source folder to a destination folder, updating metadata if available.
     *
     * @param string $sourceFolder The source folder (e.g. "root" or a subfolder)
     * @param string $destinationFolder The destination folder.
     * @param array  $files Array of file names to copy.
     * @return array Result with either "success" or "error" key.
     */
    public static function copyFiles($sourceFolder, $destinationFolder, $files) {
        $errors = [];

        list($sourceDir, $err) = self::resolveFolderPath($sourceFolder, false);
        if ($err) return ["error" => $err];
        list($destDir, $err)   = self::resolveFolderPath($destinationFolder, true);
        if ($err) return ["error" => $err];

        $sourceDir .= DIRECTORY_SEPARATOR;
        $destDir   .= DIRECTORY_SEPARATOR;

        // Metadata paths
        $srcMetaFile  = self::getMetadataFilePath($sourceFolder);
        $destMetaFile = self::getMetadataFilePath($destinationFolder);

        $srcMetadata  = file_exists($srcMetaFile)  ? (json_decode(file_get_contents($srcMetaFile), true)  ?: []) : [];
        $destMetadata = file_exists($destMetaFile) ? (json_decode(file_get_contents($destMetaFile), true) ?: []) : [];

        $safeFileNamePattern = REGEX_FILE_NAME;
        $actor = $_SESSION['username'] ?? 'Unknown';
        $now   = date(DATE_TIME_FORMAT);

        foreach ($files as $fileName) {
            $originalName = basename(trim($fileName));
            $basename     = $originalName;

            if (!preg_match($safeFileNamePattern, $basename)) {
                $errors[] = "$basename has an invalid name.";
                continue;
            }

            $srcPath  = $sourceDir . $originalName;
            $destPath = $destDir . $basename;

            clearstatcache();
            if (!file_exists($srcPath)) {
                $errors[] = "$originalName does not exist in source.";
                continue;
            }

            // Avoid overwrite: pick unique name
            if (file_exists($destPath)) {
                $basename = self::getUniqueFileName($destDir, $basename);
                $destPath = $destDir . $basename;
            }

            if (!copy($srcPath, $destPath)) {
                $errors[] = "Failed to copy $basename.";
                continue;
            }

            // Carry over non-ownership fields (e.g., tags), but stamp new ownership/timestamps
            $tags = [];
            if (isset($srcMetadata[$originalName]['tags']) && is_array($srcMetadata[$originalName]['tags'])) {
                $tags = $srcMetadata[$originalName]['tags'];
            }

            $destMetadata[$basename] = [
                'uploaded' => $now,
                'modified' => $now,
                'uploader' => $actor,
                'tags'     => $tags
            ];
        }

        if (file_put_contents($destMetaFile, json_encode($destMetadata, JSON_PRETTY_PRINT), LOCK_EX) === false) {
            $errors[] = "Failed to update destination metadata.";
        }

        return empty($errors)
            ? ["success" => "Files copied successfully"]
            : ["error" => implode("; ", $errors)];
    }

    /**
     * Generates the metadata file path for a given folder.
     *
     * @param string $folder
     * @return string
     */
    private static function getMetadataFilePath($folder) {
        if (strtolower($folder) === 'root' || trim($folder) === '') {
            return META_DIR . "root_metadata.json";
        }
        return META_DIR . str_replace(['/', '\\', ' '], '-', trim($folder)) . '_metadata.json';
    }

    /**
     * Generates a unique file name if a file with the same name exists in the destination directory.
     *
     * @param string $destDir
     * @param string $fileName
     * @return string
     */
    private static function getUniqueFileName($destDir, $fileName) {
        $fullPath = $destDir . $fileName;
        clearstatcache(true, $fullPath);
        if (!file_exists($fullPath)) {
            return $fileName;
        }
        $basename = pathinfo($fileName, PATHINFO_FILENAME);
        $extension = pathinfo($fileName, PATHINFO_EXTENSION);
        $counter = 1;
        do {
            $newName = $basename . " (" . $counter . ")" . ($extension ? "." . $extension : "");
            $newFullPath = $destDir . $newName;
            clearstatcache(true, $newFullPath);
            $counter++;
        } while (file_exists($destDir . $newName));
        return $newName;
    }

    /**
     * Deletes (i.e. moves to Trash) the specified files from a given folder
     * and updates metadata accordingly.
     *
     * @param string $folder The folder (or "root") from which files are deleted.
     * @param array $files The array of file names to delete.
     * @return array An associative array with a "success" or "error" message.
     */
    public static function deleteFiles($folder, $files) {
        $errors = [];

        list($uploadDir, $err) = self::resolveFolderPath($folder, false);
        if ($err) return ["error" => $err];
        $uploadDir .= DIRECTORY_SEPARATOR;

        // Setup the Trash folder and metadata.
        $trashDir = rtrim(TRASH_DIR, '/\\') . DIRECTORY_SEPARATOR;
        if (!file_exists($trashDir)) {
            mkdir($trashDir, 0755, true);
        }
        $trashMetadataFile = $trashDir . "trash.json";
        $trashData = file_exists($trashMetadataFile)
                     ? json_decode(file_get_contents($trashMetadataFile), true)
                     : [];
        if (!is_array($trashData)) {
            $trashData = [];
        }

        // Load folder metadata if available.
        $metadataFile = self::getMetadataFilePath($folder);
        $folderMetadata = file_exists($metadataFile)
                          ? json_decode(file_get_contents($metadataFile), true)
                          : [];
        if (!is_array($folderMetadata)) {
            $folderMetadata = [];
        }

        $movedFiles = [];
        $safeFileNamePattern = REGEX_FILE_NAME;

        foreach ($files as $fileName) {
            $basename = basename(trim($fileName));

            // Validate the file name.
            if (!preg_match($safeFileNamePattern, $basename)) {
                $errors[] = "$basename has an invalid name.";
                continue;
            }

            $filePath = $uploadDir . $basename;

            // Check if file exists.
            if (file_exists($filePath)) {
                // Unique trash name (timestamp + random)
                $trashFileName = $basename . '_' . time() . '_' . bin2hex(random_bytes(4));
                if (rename($filePath, $trashDir . $trashFileName)) {
                    $movedFiles[] = $basename;
                    // Record trash metadata for possible restoration.
                    $trashData[] = [
                        'type'           => 'file',
                        'originalFolder' => $uploadDir,
                        'originalName'   => $basename,
                        'trashName'      => $trashFileName,
                        'trashedAt'      => time(),
                        'uploaded'       => $folderMetadata[$basename]['uploaded'] ?? "Unknown",
                        'uploader'       => $folderMetadata[$basename]['uploader'] ?? "Unknown",
                        'deletedBy'      => $_SESSION['username'] ?? "Unknown"
                    ];
                } else {
                    $errors[] = "Failed to move $basename to Trash.";
                    continue;
                }
            } else {
                // If file does not exist, consider it already removed.
                $movedFiles[] = $basename;
            }
        }

        // Save updated trash metadata.
        file_put_contents($trashMetadataFile, json_encode($trashData, JSON_PRETTY_PRINT), LOCK_EX);

        // Remove deleted file entries from folder metadata.
        if (file_exists($metadataFile)) {
            $metadata = json_decode(file_get_contents($metadataFile), true);
            if (is_array($metadata)) {
                foreach ($movedFiles as $delFile) {
                    if (isset($metadata[$delFile])) {
                        unset($metadata[$delFile]);
                    }
                }
                file_put_contents($metadataFile, json_encode($metadata, JSON_PRETTY_PRINT), LOCK_EX);
            }
        }

        if (empty($errors)) {
            return ["success" => "Files moved to Trash: " . implode(", ", $movedFiles)];
        } else {
            return ["error" => implode("; ", $errors) . ". Files moved to Trash: " . implode(", ", $movedFiles)];
        }
    }

    /**
     * Moves files from a source folder to a destination folder and updates metadata.
     *
     * @param string $sourceFolder The source folder (e.g., "root" or a subfolder).
     * @param string $destinationFolder The destination folder.
     * @param array  $files An array of file names to move.
     * @return array An associative array with either a "success" key or an "error" key.
     */
    public static function moveFiles($sourceFolder, $destinationFolder, $files) {
        $errors = [];

        list($sourceDir, $err) = self::resolveFolderPath($sourceFolder, false);
        if ($err) return ["error" => $err];
        list($destDir, $err)   = self::resolveFolderPath($destinationFolder, true);
        if ($err) return ["error" => $err];

        $sourceDir .= DIRECTORY_SEPARATOR;
        $destDir   .= DIRECTORY_SEPARATOR;

        // Get metadata file paths.
        $srcMetaFile  = self::getMetadataFilePath($sourceFolder);
        $destMetaFile = self::getMetadataFilePath($destinationFolder);

        $srcMetadata  = file_exists($srcMetaFile)  ? json_decode(file_get_contents($srcMetaFile), true)  : [];
        $destMetadata = file_exists($destMetaFile) ? json_decode(file_get_contents($destMetaFile), true) : [];
        if (!is_array($srcMetadata)) {
            $srcMetadata = [];
        }
        if (!is_array($destMetadata)) {
            $destMetadata = [];
        }

        $movedFiles = [];
        $safeFileNamePattern = REGEX_FILE_NAME;

        foreach ($files as $fileName) {
            // Save the original file name for metadata lookup.
            $originalName = basename(trim($fileName));
            $basename = $originalName;

            // Validate the file name.
            if (!preg_match($safeFileNamePattern, $basename)) {
                $errors[] = "$basename has invalid characters.";
                continue;
            }

            $srcPath = $sourceDir . $originalName;
            $destPath = $destDir . $basename;

            clearstatcache();
            if (!file_exists($srcPath)) {
                $errors[] = "$originalName does not exist in source.";
                continue;
            }

            // If a file with the same name exists in destination, generate a unique name.
            if (file_exists($destPath)) {
                $uniqueName = self::getUniqueFileName($destDir, $basename);
                $basename = $uniqueName;
                $destPath = $destDir . $uniqueName;
            }

            if (!rename($srcPath, $destPath)) {
                $errors[] = "Failed to move $basename.";
                continue;
            }

            $movedFiles[] = $originalName;
            // Update destination metadata: if metadata for the original file exists in source, move it under the new name.
            if (isset($srcMetadata[$originalName])) {
                $destMetadata[$basename] = $srcMetadata[$originalName];
                unset($srcMetadata[$originalName]);
            }
        }

        // Write back updated metadata.
        if (file_put_contents($srcMetaFile, json_encode($srcMetadata, JSON_PRETTY_PRINT), LOCK_EX) === false) {
            $errors[] = "Failed to update source metadata.";
        }
        if (file_put_contents($destMetaFile, json_encode($destMetadata, JSON_PRETTY_PRINT), LOCK_EX) === false) {
            $errors[] = "Failed to update destination metadata.";
        }

        if (empty($errors)) {
            return ["success" => "Files moved successfully"];
        } else {
            return ["error" => implode("; ", $errors)];
        }
    }

    /**
     * Renames a file within a given folder and updates folder metadata.
     *
     * @param string $folder The folder where the file is located (or "root" for the base directory).
     * @param string $oldName The current name of the file.
     * @param string $newName The new name for the file.
     * @return array An associative array with either "success" (and newName) or "error" message.
     */
    public static function renameFile($folder, $oldName, $newName) {
        list($directory, $err) = self::resolveFolderPath($folder, false);
        if ($err) return ["error" => $err];
        $directory .= DIRECTORY_SEPARATOR;

        // Sanitize file names.
        $oldName = basename(trim($oldName));
        $newName = basename(trim($newName));

        // Validate file names using REGEX_FILE_NAME.
        if (!preg_match(REGEX_FILE_NAME, $oldName) || !preg_match(REGEX_FILE_NAME, $newName)) {
            return ["error" => "Invalid file name."];
        }

        $oldPath = $directory . $oldName;
        $newPath = $directory . $newName;

        // Helper: Generate a unique file name if the new name already exists.
        if (file_exists($newPath)) {
            $newName = self::getUniqueFileName($directory, $newName);
            $newPath = $directory . $newName;
        }

        // Check that the old file exists.
        if (!file_exists($oldPath)) {
            return ["error" => "File does not exist"];
        }

        // Perform the rename.
        if (rename($oldPath, $newPath)) {
            // Update the metadata file.
            $metadataKey = ($folder === 'root') ? "root" : $folder;
            $metadataFile = META_DIR . str_replace(['/', '\\', ' '], '-', trim($metadataKey)) . '_metadata.json';

            if (file_exists($metadataFile)) {
                $metadata = json_decode(file_get_contents($metadataFile), true);
                if (isset($metadata[$oldName])) {
                    $metadata[$newName] = $metadata[$oldName];
                    unset($metadata[$oldName]);
                    file_put_contents($metadataFile, json_encode($metadata, JSON_PRETTY_PRINT), LOCK_EX);
                }
            }
            return ["success" => "File renamed successfully", "newName" => $newName];
        } else {
            return ["error" => "Error renaming file"];
        }
    }

    /*
     * Save a file’s contents *and* record its metadata, including who uploaded it.
     *
     * @param string                $folder    Folder key (e.g. "root" or "invoices/2025")
     * @param string                $fileName  Basename of the file
     * @param resource|string       $content   File contents (stream or string)
     * @param string|null           $uploader  Username of uploader (if null, falls back to session)
     * @return array                          ["success"=>"…"] or ["error"=>"…"]
     */
    public static function saveFile(string $folder, string $fileName, $content, ?string $uploader = null): array {
        $folder   = trim($folder) ?: 'root';
        $fileName = basename(trim($fileName));

        if (strtolower($folder) !== 'root' && !preg_match(REGEX_FOLDER_NAME, $folder)) {
            return ["error" => "Invalid folder name"];
        }
        if (!preg_match(REGEX_FILE_NAME, $fileName)) {
            return ["error" => "Invalid file name"];
        }

        $baseDirReal = realpath(UPLOAD_DIR);
        if ($baseDirReal === false) {
            return ["error" => "Server misconfiguration"];
        }

        $targetDir = (strtolower($folder) === 'root')
            ? rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR
            : rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . trim($folder, "/\\ ") . DIRECTORY_SEPARATOR;

        // Ensure directory exists *before* realpath + containment check
        if (!is_dir($targetDir) && !mkdir($targetDir, 0775, true)) {
            return ["error" => "Failed to create destination folder"];
        }

        $targetDirReal = realpath($targetDir);
        if ($targetDirReal === false || strpos($targetDirReal, $baseDirReal) !== 0) {
            return ["error" => "Invalid folder path"];
        }

        $filePath = $targetDirReal . DIRECTORY_SEPARATOR . $fileName;

        if (is_resource($content)) {
            $out = fopen($filePath, 'wb');
            if ($out === false) return ["error" => "Unable to open file for writing"];
            stream_copy_to_stream($content, $out);
            fclose($out);
        } else {
            if (file_put_contents($filePath, (string)$content, LOCK_EX) === false) {
                return ["error" => "Error saving file"];
            }
        }

        // Metadata
        $metadataKey      = strtolower($folder) === "root" ? "root" : $folder;
        $metadataFileName = str_replace(['/', '\\', ' '], '-', trim($metadataKey)) . '_metadata.json';
        $metadataFilePath = META_DIR . $metadataFileName;

        $metadata = file_exists($metadataFilePath) ? (json_decode(file_get_contents($metadataFilePath), true) ?: []) : [];

        $currentTime = date(DATE_TIME_FORMAT);
        $uploader = $uploader ?? ($_SESSION['username'] ?? "Unknown");

        if (isset($metadata[$fileName])) {
            $metadata[$fileName]['modified'] = $currentTime;
            $metadata[$fileName]['uploader'] = $uploader;
        } else {
            $metadata[$fileName] = [
                "uploaded" => $currentTime,
                "modified" => $currentTime,
                "uploader" => $uploader
            ];
        }

        if (file_put_contents($metadataFilePath, json_encode($metadata, JSON_PRETTY_PRINT), LOCK_EX) === false) {
            return ["error" => "Failed to update metadata"];
        }

        return ["success" => "File saved successfully"];
    }

    /**
     * Validates and retrieves information needed to download a file.
     *
     * @param string $folder The folder from which to download (e.g., "root" or a subfolder).
     * @param string $file The file name.
     * @return array An associative array with "error" key on failure,
     *               or "filePath" and "mimeType" keys on success.
     */
    public static function getDownloadInfo($folder, $file) {
        // Validate file name using REGEX_FILE_NAME.
        $file = basename(trim($file));
        if (!preg_match(REGEX_FILE_NAME, $file)) {
            return ["error" => "Invalid file name."];
        }

        // Determine the real upload directory.
        $uploadDirReal = realpath(UPLOAD_DIR);
        if ($uploadDirReal === false) {
            return ["error" => "Server misconfiguration."];
        }

        // Determine directory based on folder.
        if (strtolower($folder) === 'root' || trim($folder) === '') {
            $directory = $uploadDirReal;
        } else {
            // Prevent path traversal.
            if (strpos($folder, '..') !== false) {
                return ["error" => "Invalid folder name."];
            }
            $directoryPath = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . trim($folder, "/\\ ");
            $directory = realpath($directoryPath);
            if ($directory === false || strpos($directory, $uploadDirReal) !== 0) {
                return ["error" => "Invalid folder path."];
            }
        }

        // Build the file path.
        $filePath = $directory . DIRECTORY_SEPARATOR . $file;
        $realFilePath = realpath($filePath);

        // Ensure the file exists and is within the allowed directory.
        if ($realFilePath === false || strpos($realFilePath, $uploadDirReal) !== 0) {
            return ["error" => "Access forbidden."];
        }
        if (!file_exists($realFilePath)) {
            return ["error" => "File not found."];
        }

        // Get the MIME type with safe fallback.
        $mimeType = function_exists('mime_content_type') ? mime_content_type($realFilePath) : null;
        if (!$mimeType) {
            $mimeType = 'application/octet-stream';
        }

        return [
            "filePath" => $realFilePath,
            "mimeType" => $mimeType
        ];
    }

    /**
     * Creates a ZIP archive of the specified files from a given folder.
     *
     * @param string $folder The folder from which to zip the files (e.g., "root" or a subfolder).
     * @param array $files An array of file names to include in the ZIP.
     * @return array An associative array with either an "error" key or a "zipPath" key.
     */
    public static function createZipArchive($folder, $files) {
        // Purge old temp zips > 6h (best-effort)
        $zipRoot = rtrim((string)META_DIR, '/\\') . DIRECTORY_SEPARATOR . 'ziptmp';
        $now = time();
        foreach ((glob($zipRoot . DIRECTORY_SEPARATOR . 'download-*.zip') ?: []) as $zp) {
            if (is_file($zp) && ($now - (int)@filemtime($zp)) > 21600) { @unlink($zp); }
        }
    
        // Normalize and validate target folder
        $folder = trim((string)$folder) ?: 'root';
        $baseDir = realpath(UPLOAD_DIR);
        if ($baseDir === false) {
            return ["error" => "Uploads directory not configured correctly."];
        }
    
        if (strtolower($folder) === 'root' || $folder === "") {
            $folderPathReal = $baseDir;
        } else {
            if (strpos($folder, '..') !== false) {
                return ["error" => "Invalid folder name."];
            }
            $parts = explode('/', trim($folder, "/\\ "));
            foreach ($parts as $part) {
                if ($part === '' || !preg_match(REGEX_FOLDER_NAME, $part)) {
                    return ["error" => "Invalid folder name."];
                }
            }
            $folderPath = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . implode(DIRECTORY_SEPARATOR, $parts);
            $folderPathReal = realpath($folderPath);
            if ($folderPathReal === false || strpos($folderPathReal, $baseDir) !== 0) {
                return ["error" => "Folder not found."];
            }
        }
    
        // Collect files to zip (only regular files in the chosen folder)
        $filesToZip = [];
        foreach ($files as $fileName) {
            $fileName = basename(trim((string)$fileName));
            if (!preg_match(REGEX_FILE_NAME, $fileName)) {
                continue;
            }
            $fullPath = $folderPathReal . DIRECTORY_SEPARATOR . $fileName;
            // Skip symlinks (avoid archiving outside targets via links)
            if (is_link($fullPath)) {
                continue;
            }
            if (is_file($fullPath)) {
                $filesToZip[] = $fullPath;
            }
        }
        if (empty($filesToZip)) {
            return ["error" => "No valid files found to zip."];
        }
    
        // Workspace on the big disk: META_DIR/ziptmp
        $work = rtrim((string)META_DIR, '/\\') . DIRECTORY_SEPARATOR . 'ziptmp';
        if (!is_dir($work)) { @mkdir($work, 0775, true); }
        if (!is_dir($work) || !is_writable($work)) {
            return ["error" => "ZIP temp dir not writable: " . $work];
        }
    
        // Optional sanity: ensure there is roughly enough free space
        $totalSize = 0;
        foreach ($filesToZip as $fp) {
            $sz = @filesize($fp);
            if ($sz !== false) $totalSize += (int)$sz;
        }
        $free = @disk_free_space($work);
        // Add ~20MB overhead and a 5% cushion
        if ($free !== false && $totalSize > 0) {
            $needed = (int)ceil($totalSize * 1.05) + (20 * 1024 * 1024);
            if ($free < $needed) {
                return ["error" => "Insufficient free space in ZIP workspace."];
            }
        }
    
        @set_time_limit(0);
    
        // Create the ZIP path inside META_DIR/ziptmp (libzip temp stays on same FS)
        $zipName = 'download-' . date('Ymd-His') . '-' . bin2hex(random_bytes(4)) . '.zip';
        $zipPath = $work . DIRECTORY_SEPARATOR . $zipName;
    
        $zip = new \ZipArchive();
        if ($zip->open($zipPath, \ZipArchive::CREATE | \ZipArchive::OVERWRITE) !== true) {
            return ["error" => "Could not create zip archive."];
        }
    
        foreach ($filesToZip as $filePath) {
            // Add using basename at the root of the zip (matches current behavior)
            $zip->addFile($filePath, basename($filePath));
        }
    
        if (!$zip->close()) {
            // Commonly indicates disk full at finalize
            return ["error" => "Failed to finalize ZIP (disk full?)."];
        }
    
        // Success: controller will readfile() and unlink()
        return ["zipPath" => $zipPath];
    }

    /**
     * Extracts ZIP archives from the specified folder.
     *
     * @param string $folder The folder from which ZIP files will be extracted (e.g., "root" or a subfolder).
     * @param array $files An array of ZIP file names to extract.
     * @return array An associative array with keys "success" (boolean), and either "extractedFiles" (array) on success or "error" (string) on failure.
     */
    public static function extractZipArchive($folder, $files) {
        $errors = [];
        $allSuccess = true;
        $extractedFiles = [];
    
        // Config toggles
        $SKIP_DOTFILES = defined('SKIP_DOTFILES_ON_EXTRACT') ? (bool)SKIP_DOTFILES_ON_EXTRACT : true;
    
        // Hard limits to mitigate zip-bombs (tweak via defines if you like)
        $MAX_UNZIP_BYTES = defined('MAX_UNZIP_BYTES') ? (int)MAX_UNZIP_BYTES : (200 * 1024 * 1024 * 1024); // 200 GiB
        $MAX_UNZIP_FILES = defined('MAX_UNZIP_FILES') ? (int)MAX_UNZIP_FILES : 20000;
    
        $baseDir = realpath(UPLOAD_DIR);
        if ($baseDir === false) {
            return ["error" => "Uploads directory not configured correctly."];
        }
    
        // Build target dir
        if (strtolower(trim($folder) ?: '') === "root") {
            $relativePath = "";
            $folderNorm = "root";
        } else {
            $parts = explode('/', trim($folder, "/\\"));
            foreach ($parts as $part) {
                if ($part === '' || $part === '.' || $part === '..' || !preg_match(REGEX_FOLDER_NAME, $part)) {
                    return ["error" => "Invalid folder name."];
                }
            }
            $relativePath = implode(DIRECTORY_SEPARATOR, $parts) . DIRECTORY_SEPARATOR;
            $folderNorm   = implode('/', $parts); // normalized with forward slashes for metadata helpers
        }
    
        $folderPath = $baseDir . DIRECTORY_SEPARATOR . $relativePath;
        if (!is_dir($folderPath) && !mkdir($folderPath, 0775, true)) {
            return ["error" => "Folder not found and cannot be created."];
        }
        $folderPathReal = realpath($folderPath);
        if ($folderPathReal === false || strpos($folderPathReal, $baseDir) !== 0) {
            return ["error" => "Folder not found."];
        }
    
        // Metadata cache per folder to avoid many reads/writes
        $metaCache = [];
        $getMeta = function(string $folderStr) use (&$metaCache) {
            if (!isset($metaCache[$folderStr])) {
                $mf = self::getMetadataFilePath($folderStr);
                $metaCache[$folderStr] = file_exists($mf) ? (json_decode(file_get_contents($mf), true) ?: []) : [];
            }
            return $metaCache[$folderStr];
        };
        $putMeta = function(string $folderStr, array $meta) use (&$metaCache) {
            $metaCache[$folderStr] = $meta;
        };
    
        $safeFileNamePattern = REGEX_FILE_NAME;
        $actor = $_SESSION['username'] ?? 'Unknown';
        $now   = date(DATE_TIME_FORMAT);
    
        // --- Helpers ---
    
        // Reject absolute paths, traversal, drive letters
        $isUnsafeEntryPath = function(string $entry) : bool {
            $e = str_replace('\\', '/', $entry);
            if ($e === '' || str_contains($e, "\0")) return true;
            if (str_starts_with($e, '/')) return true;                 // absolute nix path
            if (preg_match('/^[A-Za-z]:[\\/]/', $e)) return true;      // Windows drive
            if (str_contains($e, '../') || str_contains($e, '..\\')) return true;
            return false;
        };
    
        // Validate each subfolder name in the path using REGEX_FOLDER_NAME
        $validEntrySubdirs = function(string $entry) : bool {
            $e = trim(str_replace('\\', '/', $entry), '/');
            if ($e === '') return true;
            $dirs = explode('/', $e);
            array_pop($dirs); // remove basename; we only validate directories here
            foreach ($dirs as $d) {
                if ($d === '' || !preg_match(REGEX_FOLDER_NAME, $d)) return false;
            }
            return true;
        };
    
        // NEW: hidden path detector — true if ANY segment starts with '.'
        $isHiddenDotPath = function(string $entry) : bool {
            $e = trim(str_replace('\\', '/', $entry), '/');
            if ($e === '') return false;
            foreach (explode('/', $e) as $seg) {
                if ($seg !== '' && $seg[0] === '.') return true;
            }
            return false;
        };
    
        // Generalized metadata stamper: writes to the specified folder's metadata.json
        $stampMeta = function(string $folderStr, string $basename) use (&$getMeta, &$putMeta, $actor, $now) {
            $meta = $getMeta($folderStr);
            $meta[$basename] = [
                'uploaded' => $now,
                'modified' => $now,
                'uploader' => $actor,
            ];
            $putMeta($folderStr, $meta);
        };
    
        // No PHP execution time limit during heavy work
        @set_time_limit(0);
    
        foreach ($files as $zipFileName) {
            $zipBase = basename(trim((string)$zipFileName));
            if (strtolower(substr($zipBase, -4)) !== '.zip') {
                continue;
            }
            if (!preg_match($safeFileNamePattern, $zipBase)) {
                $errors[] = "$zipBase has an invalid name.";
                $allSuccess = false;
                continue;
            }
    
            $zipFilePath = $folderPathReal . DIRECTORY_SEPARATOR . $zipBase;
            if (!file_exists($zipFilePath)) {
                $errors[] = "$zipBase does not exist in folder.";
                $allSuccess = false;
                continue;
            }
    
            $zip = new \ZipArchive();
            if ($zip->open($zipFilePath) !== true) {
                $errors[] = "Could not open $zipBase as a zip file.";
                $allSuccess = false;
                continue;
            }
    
            // ---- Pre-scan: safety and size limits + build allow-list (skip dotfiles) ----
            $unsafe = false;
            $totalUncompressed = 0;
            $fileCount = 0;
            $allowedEntries = [];   // names to extract (files and/or directories)
            $allowedFiles   = [];   // only files (for metadata stamping)
    
            for ($i = 0; $i < $zip->numFiles; $i++) {
                $stat = $zip->statIndex($i);
                $name = $zip->getNameIndex($i);
                if ($name === false || !$stat) { $unsafe = true; break; }
    
                $isDir = str_ends_with($name, '/');
    
                // Basic path checks
                if ($isUnsafeEntryPath($name) || !$validEntrySubdirs($name)) { $unsafe = true; break; }
    
                // Skip hidden entries (any segment starts with '.')
                if ($SKIP_DOTFILES && $isHiddenDotPath($name)) {
                    continue; // just ignore; do not treat as unsafe
                }
    
                // Detect symlinks via external attributes (best-effort)
                $mode = (isset($stat['external_attributes']) ? (($stat['external_attributes'] >> 16) & 0xF000) : 0);
                if ($mode === 0120000) { // S_IFLNK
                    $unsafe = true; break;
                }
    
                // Track limits only for files we're going to extract
                if (!$isDir) {
                    $fileCount++;
                    $sz = isset($stat['size']) ? (int)$stat['size'] : 0;
                    $totalUncompressed += $sz;
                    if ($fileCount > $MAX_UNZIP_FILES || $totalUncompressed > $MAX_UNZIP_BYTES) {
                        $unsafe = true; break;
                    }
                    $allowedFiles[] = $name;
                }
    
                $allowedEntries[] = $name;
            }
    
            if ($unsafe) {
                $zip->close();
                $errors[] = "$zipBase contains unsafe or oversized contents; extraction aborted.";
                $allSuccess = false;
                continue;
            }
    
            // Nothing to extract after filtering?
            if (empty($allowedEntries)) {
                $zip->close();
                // Treat as success (nothing visible to extract), but informatively note it
                $errors[] = "$zipBase contained only hidden or unsupported entries.";
                $allSuccess = false; // or keep true if you'd rather not mark as failure
                continue;
            }
    
            // ---- Extract ONLY the allowed entries ----
            if (!$zip->extractTo($folderPathReal, $allowedEntries)) {
                $errors[] = "Failed to extract $zipBase.";
                $allSuccess = false;
                $zip->close();
                continue;
            }
    
            // ---- Stamp metadata for files in the target folder AND nested subfolders (allowed files only) ----
            foreach ($allowedFiles as $entryName) {
                // Normalize entry path for filesystem checks
                $entryFsRel = str_replace(['\\'], '/', $entryName);
                $entryFsRel = ltrim($entryFsRel, '/'); // ensure relative
    
                // Skip any directories (shouldn't be listed here, but defend anyway)
                if ($entryFsRel === '' || str_ends_with($entryFsRel, '/')) continue;
    
                $basename = basename($entryFsRel);
                if ($basename === '' || !preg_match($safeFileNamePattern, $basename)) continue;
    
                // Decide which folder's metadata to update:
                // - top-level files -> $folderNorm
                // - nested files    -> corresponding "<folderNorm>/<sub/dir>" (or "sub/dir" if folderNorm is 'root')
                $relDir = str_replace('\\', '/', trim(dirname($entryFsRel), '.'));
                $relDir = ($relDir === '.' ? '' : trim($relDir, '/'));
    
                $targetFolderNorm = ($relDir === '' || $relDir === '.')
                    ? $folderNorm
                    : (($folderNorm === 'root') ? $relDir : ($folderNorm . '/' . $relDir));
    
                // Only stamp if the file actually exists on disk after extraction
                $targetAbs = $folderPathReal . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, $entryFsRel);
                if (is_file($targetAbs)) {
                    // Preserve list behavior: only include top-level extracted names
                    if ($relDir === '' || $relDir === '.') {
                        $extractedFiles[] = $basename;
                    }
                    $stampMeta($targetFolderNorm, $basename);
                }
            }
    
            $zip->close();
        }
    
        // Persist metadata for any touched folder(s)
        foreach ($metaCache as $folderStr => $meta) {
            $metadataFile = self::getMetadataFilePath($folderStr);
            if (!is_dir(dirname($metadataFile))) {
                @mkdir(dirname($metadataFile), 0775, true);
            }
            if (file_put_contents($metadataFile, json_encode($meta, JSON_PRETTY_PRINT), LOCK_EX) === false) {
                $errors[] = "Failed to update metadata for {$folderStr}.";
                $allSuccess = false;
            }
        }
    
        return $allSuccess
            ? ["success" => true, "extractedFiles" => $extractedFiles]
            : ["success" => false, "error" => implode(" ", $errors)];
    }

    /**
     * Retrieves the share record for a given token.
     *
     * @param string $token The share token.
     * @return array|null Returns the share record as an associative array, or null if not found.
     */
    public static function getShareRecord($token) {
        $shareFile = META_DIR . "share_links.json";
        if (!file_exists($shareFile)) {
            return null;
        }
        $shareLinks = json_decode(file_get_contents($shareFile), true);
        if (!is_array($shareLinks) || !isset($shareLinks[$token])) {
            return null;
        }
        return $shareLinks[$token];
    }

    /**
     * Creates a share link for a file.
     *
     * @param string $folder The folder containing the shared file (or "root").
     * @param string $file The name of the file being shared.
     * @param int $expirationSeconds The number of seconds until expiration.
     * @param string $password Optional password protecting the share.
     * @return array Returns an associative array with keys "token" and "expires" on success,
     *               or "error" on failure.
     */
    public static function createShareLink($folder, $file, $expirationSeconds = 3600, $password = "") {
        // Validate folder if necessary (this can also be done in the controller).
        if (strtolower($folder) !== 'root' && !preg_match(REGEX_FOLDER_NAME, $folder)) {
            return ["error" => "Invalid folder name."];
        }
        // Validate file name.
        $file = basename(trim($file));
        if (!preg_match(REGEX_FILE_NAME, $file)) {
            return ["error" => "Invalid file name."];
        }

        // Generate a secure token (32 hex characters).
        $token = bin2hex(random_bytes(16));

        // Calculate expiration (Unix timestamp).
        $expires = time() + $expirationSeconds;

        // Hash the password if provided.
        $hashedPassword = !empty($password) ? password_hash($password, PASSWORD_DEFAULT) : "";

        // File to store share links.
        $shareFile = META_DIR . "share_links.json";
        $shareLinks = [];
        if (file_exists($shareFile)) {
            $data = file_get_contents($shareFile);
            $shareLinks = json_decode($data, true);
            if (!is_array($shareLinks)) {
                $shareLinks = [];
            }
        }

        // Clean up expired share links.
        $currentTime = time();
        foreach ($shareLinks as $key => $link) {
            if ($link["expires"] < $currentTime) {
                unset($shareLinks[$key]);
            }
        }

        // Add new share record.
        $shareLinks[$token] = [
            "folder"   => $folder,
            "file"     => $file,
            "expires"  => $expires,
            "password" => $hashedPassword
        ];

        // Save the updated share links.
        if (file_put_contents($shareFile, json_encode($shareLinks, JSON_PRETTY_PRINT), LOCK_EX)) {
            return ["token" => $token, "expires" => $expires];
        } else {
            return ["error" => "Could not save share link."];
        }
    }

    /**
     * Retrieves and enriches trash records from the trash metadata file.
     *
     * @return array An array of trash items.
     */
    public static function getTrashItems() {
        $trashDir = rtrim(TRASH_DIR, '/\\') . DIRECTORY_SEPARATOR;
        $trashMetadataFile = $trashDir . "trash.json";
        $trashItems = [];
        if (file_exists($trashMetadataFile)) {
            $json = file_get_contents($trashMetadataFile);
            $trashItems = json_decode($json, true);
            if (!is_array($trashItems)) {
                $trashItems = [];
            }
        }

        // Enrich each trash record.
        foreach ($trashItems as &$item) {
            if (empty($item['deletedBy'])) {
                $item['deletedBy'] = "Unknown";
            }
            if (empty($item['uploaded']) || empty($item['uploader'])) {
                if (isset($item['originalFolder']) && isset($item['originalName'])) {
                    $metadataFile = self::getMetadataFilePath($item['originalFolder']);
                    if (file_exists($metadataFile)) {
                        $metadata = json_decode(file_get_contents($metadataFile), true);
                        if (is_array($metadata) && isset($metadata[$item['originalName']])) {
                            $item['uploaded'] = !empty($metadata[$item['originalName']]['uploaded']) ? $metadata[$item['originalName']]['uploaded'] : "Unknown";
                            $item['uploader'] = !empty($metadata[$item['originalName']]['uploader']) ? $metadata[$item['originalName']]['uploader'] : "Unknown";
                        } else {
                            $item['uploaded'] = "Unknown";
                            $item['uploader'] = "Unknown";
                        }
                    } else {
                        $item['uploaded'] = "Unknown";
                        $item['uploader'] = "Unknown";
                    }
                } else {
                    $item['uploaded'] = "Unknown";
                    $item['uploader'] = "Unknown";
                }
            }
        }
        unset($item);
        return $trashItems;
    }

    /**
     * Restores files from Trash based on an array of trash file identifiers.
     *
     * @param array $trashFiles An array of trash file names (i.e. the 'trashName' fields).
     * @return array An associative array with keys "restored" (an array of successfully restored items)
     *               and optionally an "error" message if any issues occurred.
     */
    public static function restoreFiles(array $trashFiles) {
        $errors = [];
        $restoredItems = [];

        // Setup Trash directory and trash metadata file.
        $trashDir = rtrim(TRASH_DIR, '/\\') . DIRECTORY_SEPARATOR;
        if (!file_exists($trashDir)) {
            mkdir($trashDir, 0755, true);
        }
        $trashMetadataFile = $trashDir . "trash.json";
        $trashData = [];
        if (file_exists($trashMetadataFile)) {
            $json = file_get_contents($trashMetadataFile);
            $trashData = json_decode($json, true);
            if (!is_array($trashData)) {
                $trashData = [];
            }
        }

        // Helper to get metadata file path for a folder.
        $getMetadataFilePath = function($folder) {
            if (strtolower($folder) === 'root' || trim($folder) === '') {
                return META_DIR . "root_metadata.json";
            }
            return META_DIR . str_replace(['/', '\\', ' '], '-', trim($folder)) . '_metadata.json';
        };

        // Process each provided trash file name.
        foreach ($trashFiles as $trashFileName) {
            $trashFileName = trim($trashFileName);
            // Validate file name with REGEX_FILE_NAME.
            if (!preg_match(REGEX_FILE_NAME, $trashFileName)) {
                $errors[] = "$trashFileName has an invalid format.";
                continue;
            }

            // Locate the matching trash record.
            $recordKey = null;
            foreach ($trashData as $key => $record) {
                if (isset($record['trashName']) && $record['trashName'] === $trashFileName) {
                    $recordKey = $key;
                    break;
                }
            }
            if ($recordKey === null) {
                $errors[] = "No trash record found for $trashFileName.";
                continue;
            }

            $record = $trashData[$recordKey];
            if (!isset($record['originalFolder']) || !isset($record['originalName'])) {
                $errors[] = "Incomplete trash record for $trashFileName.";
                continue;
            }
            $originalFolder = $record['originalFolder'];
            $originalName = $record['originalName'];

            // Convert absolute original folder to relative folder.
            $relativeFolder = 'root';
            if (strpos($originalFolder, UPLOAD_DIR) === 0) {
                $relativeFolder = trim(substr($originalFolder, strlen(UPLOAD_DIR)), '/\\');
                if ($relativeFolder === '') {
                    $relativeFolder = 'root';
                }
            }

            // Build destination path.
            $destinationPath = (strtolower($relativeFolder) !== 'root')
                ? rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $relativeFolder . DIRECTORY_SEPARATOR . $originalName
                : rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $originalName;

            // Handle folder-type records if necessary.
            if (isset($record['type']) && $record['type'] === 'folder') {
                if (!file_exists($destinationPath)) {
                    if (mkdir($destinationPath, 0755, true)) {
                        $restoredItems[] = $originalName . " (folder restored)";
                    } else {
                        $errors[] = "Failed to restore folder $originalName.";
                        continue;
                    }
                } else {
                    $errors[] = "Folder already exists at destination: $originalName.";
                    continue;
                }
                unset($trashData[$recordKey]);
                continue;
            }

            // For files: Ensure destination directory exists.
            $destinationDir = dirname($destinationPath);
            if (!file_exists($destinationDir)) {
                if (!mkdir($destinationDir, 0755, true)) {
                    $errors[] = "Failed to create destination folder for $originalName.";
                    continue;
                }
            }

            if (file_exists($destinationPath)) {
                $errors[] = "File already exists at destination: $originalName.";
                continue;
            }

            // Move the file from trash to its original location.
            $sourcePath = $trashDir . $trashFileName;
            if (file_exists($sourcePath)) {
                if (rename($sourcePath, $destinationPath)) {
                    $restoredItems[] = $originalName;

                    // Update metadata: Restore metadata for this file.
                    $metadataFile = $getMetadataFilePath($relativeFolder);
                    $metadata = [];
                    if (file_exists($metadataFile)) {
                        $metadata = json_decode(file_get_contents($metadataFile), true);
                        if (!is_array($metadata)) {
                            $metadata = [];
                        }
                    }
                    $restoredMeta = [
                        "uploaded" => isset($record['uploaded']) ? $record['uploaded'] : date(DATE_TIME_FORMAT),
                        "uploader" => isset($record['uploader']) ? $record['uploader'] : "Unknown"
                    ];
                    $metadata[$originalName] = $restoredMeta;
                    file_put_contents($metadataFile, json_encode($metadata, JSON_PRETTY_PRINT), LOCK_EX);
                    unset($trashData[$recordKey]);
                } else {
                    $errors[] = "Failed to restore $originalName.";
                }
            } else {
                $errors[] = "Trash file not found: $trashFileName.";
            }
        }

        // Write back updated trash metadata.
        file_put_contents($trashMetadataFile, json_encode(array_values($trashData), JSON_PRETTY_PRINT), LOCK_EX);

        if (empty($errors)) {
            return ["success" => "Items restored: " . implode(", ", $restoredItems), "restored" => $restoredItems];
        } else {
            return ["success" => false, "error" => implode("; ", $errors), "restored" => $restoredItems];
        }
    }

    /**
     * Deletes trash items based on an array of trash file identifiers.
     *
     * @param array $filesToDelete An array of trash file names (identifiers).
     * @return array An associative array containing "deleted" (array of deleted items) and optionally "error" (error message).
     */
    public static function deleteTrashFiles(array $filesToDelete) {
        // Setup trash directory and metadata file.
        $trashDir = rtrim(TRASH_DIR, '/\\') . DIRECTORY_SEPARATOR;
        if (!file_exists($trashDir)) {
            mkdir($trashDir, 0755, true);
        }
        $trashMetadataFile = $trashDir . "trash.json";

        // Load trash metadata into an associative array keyed by trashName.
        $trashData = [];
        if (file_exists($trashMetadataFile)) {
            $json = file_get_contents($trashMetadataFile);
            $tempData = json_decode($json, true);
            if (is_array($tempData)) {
                foreach ($tempData as $item) {
                    if (isset($item['trashName'])) {
                        $trashData[$item['trashName']] = $item;
                    }
                }
            }
        }

        $deletedFiles = [];
        $errors = [];

        // Define a safe file name pattern.
        $safeFileNamePattern = REGEX_FILE_NAME;

        // Process each file identifier in the $filesToDelete array.
        foreach ($filesToDelete as $trashName) {
            $trashName = trim($trashName);
            if (!preg_match($safeFileNamePattern, $trashName)) {
                $errors[] = "$trashName has an invalid format.";
                continue;
            }
            if (!isset($trashData[$trashName])) {
                $errors[] = "Trash item $trashName not found.";
                continue;
            }
            // Build the full path to the trash file.
            $filePath = $trashDir . $trashName;
            if (file_exists($filePath)) {
                if (unlink($filePath)) {
                    $deletedFiles[] = $trashName;
                    unset($trashData[$trashName]);
                } else {
                    $errors[] = "Failed to delete $trashName.";
                }
            } else {
                // If the file doesn't exist, remove its metadata.
                unset($trashData[$trashName]);
                $deletedFiles[] = $trashName;
            }
        }

        // Save the updated trash metadata back as an indexed array.
        file_put_contents($trashMetadataFile, json_encode(array_values($trashData), JSON_PRETTY_PRINT), LOCK_EX);

        if (empty($errors)) {
            return ["deleted" => $deletedFiles];
        } else {
            return ["deleted" => $deletedFiles, "error" => implode("; ", $errors)];
        }
    }

    /**
     * Retrieves file tags from the createdTags.json metadata file.
     *
     * @return array An array of tags. Returns an empty array if the file doesn't exist or is not readable.
     */
    public static function getFileTags(): array {
        $metadataPath = META_DIR . 'createdTags.json';

        // Check if the metadata file exists and is readable.
        if (!file_exists($metadataPath) || !is_readable($metadataPath)) {
            error_log('Metadata file does not exist or is not readable: ' . $metadataPath);
            return [];
        }

        $data = file_get_contents($metadataPath);
        if ($data === false) {
            error_log('Failed to read metadata file: ' . $metadataPath);
            // Return an empty array for a graceful fallback.
            return [];
        }

        $jsonData = json_decode($data, true);
        if (json_last_error() !== JSON_ERROR_NONE) {
            error_log('Invalid JSON in metadata file: ' . $metadataPath . ' Error: ' . json_last_error_msg());
            return [];
        }

        return $jsonData;
    }

    /**
     * Saves tag data for a specified file and updates the global tags.
     *
     * @param string $folder The folder where the file is located (e.g., "root" or a subfolder).
     * @param string $file The name of the file for which tags are being saved.
     * @param array  $tags An array of tag definitions, each being an associative array (e.g. ['name' => 'Tag1', 'color' => '#FF0000']).
     * @param bool   $deleteGlobal Optional flag; if true and 'tagToDelete' is provided, remove that tag from the global tags.
     * @param string|null $tagToDelete Optional tag name to delete from global tags when $deleteGlobal is true.
     * @return array Returns an associative array with a "success" key and updated "globalTags", or an "error" key on failure.
     */
    public static function saveFileTag(string $folder, string $file, array $tags, bool $deleteGlobal = false, ?string $tagToDelete = null): array {
        // Validate the file name and folder
        $folder = trim($folder) ?: 'root';
        $file   = basename(trim($file));
        if (strtolower($folder) !== 'root' && !preg_match(REGEX_FOLDER_NAME, $folder)) {
            return ["error" => "Invalid folder name."];
        }
        if (!preg_match(REGEX_FILE_NAME, $file)) {
            return ["error" => "Invalid file name."];
        }

        // Determine the folder metadata file.
        $metadataFile = (strtolower($folder) === "root")
            ? META_DIR . "root_metadata.json"
            : META_DIR . str_replace(['/', '\\', ' '], '-', trim($folder)) . '_metadata.json';

        // Load existing metadata for this folder.
        $metadata = [];
        if (file_exists($metadataFile)) {
            $metadata = json_decode(file_get_contents($metadataFile), true) ?? [];
        }

        // Update the metadata for the specified file.
        if (!isset($metadata[$file])) {
            $metadata[$file] = [];
        }
        $metadata[$file]['tags'] = $tags;

        if (file_put_contents($metadataFile, json_encode($metadata, JSON_PRETTY_PRINT), LOCK_EX) === false) {
            return ["error" => "Failed to save tag data for file metadata."];
        }

        // Now update the global tags file.
        $globalTagsFile = META_DIR . "createdTags.json";
        $globalTags = [];
        if (file_exists($globalTagsFile)) {
            $globalTags = json_decode(file_get_contents($globalTagsFile), true) ?? [];
            if (!is_array($globalTags)) {
                $globalTags = [];
            }
        }

        // If deleteGlobal is true and tagToDelete is provided, remove that tag.
        if ($deleteGlobal && !empty($tagToDelete)) {
            $tagToDeleteLower = strtolower($tagToDelete);
            $globalTags = array_values(array_filter($globalTags, function($globalTag) use ($tagToDeleteLower) {
                return strtolower($globalTag['name']) !== $tagToDeleteLower;
            }));
        } else {
            // Otherwise, merge (update or add) new tags into the global tags.
            foreach ($tags as $tag) {
                $found = false;
                foreach ($globalTags as &$globalTag) {
                    if (strtolower($globalTag['name']) === strtolower($tag['name'])) {
                        $globalTag['color'] = $tag['color'];
                        $found = true;
                        break;
                    }
                }
                if (!$found) {
                    $globalTags[] = $tag;
                }
            }
            unset($globalTag);
        }

        if (file_put_contents($globalTagsFile, json_encode($globalTags, JSON_PRETTY_PRINT), LOCK_EX) === false) {
            return ["error" => "Failed to save global tags."];
        }

        return ["success" => "Tag data saved successfully.", "globalTags" => $globalTags];
    }

    /**
     * Retrieves the list of files in a given folder, enriched with metadata, along with global tags.
     *
     * @param string $folder The folder name (e.g., "root" or a subfolder).
     * @return array Returns an associative array with keys "files" and "globalTags".
     */
    public static function getFileList(string $folder): array {
        // --- caps for safe inlining ---
        if (!defined('LISTING_CONTENT_BYTES_MAX')) define('LISTING_CONTENT_BYTES_MAX', 8192);          // 8 KB snippet
        if (!defined('INDEX_TEXT_BYTES_MAX'))    define('INDEX_TEXT_BYTES_MAX', 5 * 1024 * 1024);     // only sample files ≤ 5 MB

        $folder = trim($folder) ?: 'root';

        // Determine the target directory.
        if (strtolower($folder) !== 'root') {
            $directory = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $folder;
        } else {
            $directory = UPLOAD_DIR;
        }

        // Validate folder.
        if (strtolower($folder) !== 'root' && !preg_match(REGEX_FOLDER_NAME, $folder)) {
            return ["error" => "Invalid folder name."];
        }

        // Helper: Build the metadata file path.
        $getMetadataFilePath = function(string $folder): string {
            if (strtolower($folder) === 'root' || trim($folder) === '') {
                return META_DIR . "root_metadata.json";
            }
            return META_DIR . str_replace(['/', '\\', ' '], '-', trim($folder)) . '_metadata.json';
        };
        $metadataFile = $getMetadataFilePath($folder);
        $metadata = file_exists($metadataFile) ? (json_decode(file_get_contents($metadataFile), true) ?: []) : [];

        if (!is_dir($directory)) {
            return ["error" => "Directory not found."];
        }

        $allFiles = array_values(array_diff(scandir($directory), array('.', '..')));
        $fileList = [];

        // Define a safe file name pattern.
        $safeFileNamePattern = REGEX_FILE_NAME;

        // Prepare finfo (if available) for MIME sniffing.
        $finfo = function_exists('finfo_open') ? @finfo_open(FILEINFO_MIME_TYPE) : false;

        foreach ($allFiles as $file) {
            if ($file === '' || $file[0] === '.') {
                continue; // Skip hidden/invalid entries.
            }

            $filePath = $directory . DIRECTORY_SEPARATOR . $file;
            if (!is_file($filePath)) {
                continue; // Only process files.
            }
            if (!preg_match($safeFileNamePattern, $file)) {
                continue;
            }

            // Meta
            $mtime = @filemtime($filePath);
            $fileDateModified = $mtime ? date(DATE_TIME_FORMAT, $mtime) : "Unknown";
            $metaKey = $file;
            $fileUploadedDate = isset($metadata[$metaKey]["uploaded"]) ? $metadata[$metaKey]["uploaded"] : "Unknown";
            $fileUploader = isset($metadata[$metaKey]["uploader"]) ? $metadata[$metaKey]["uploader"] : "Unknown";

            // Size
            $fileSizeBytes = @filesize($filePath);
            if (!is_int($fileSizeBytes)) $fileSizeBytes = 0;
            if ($fileSizeBytes >= 1073741824) {
                $fileSizeFormatted = sprintf("%.1f GB", $fileSizeBytes / 1073741824);
            } elseif ($fileSizeBytes >= 1048576) {
                $fileSizeFormatted = sprintf("%.1f MB", $fileSizeBytes / 1048576);
            } elseif ($fileSizeBytes >= 1024) {
                $fileSizeFormatted = sprintf("%.1f KB", $fileSizeBytes / 1024);
            } else {
                $fileSizeFormatted = sprintf("%s bytes", number_format($fileSizeBytes));
            }

            // MIME + text detection (fallback to extension)
            $mime = 'application/octet-stream';
            if ($finfo) {
                $det = @finfo_file($finfo, $filePath);
                if (is_string($det) && $det !== '') $mime = $det;
            }
            $isTextByMime = (strpos((string)$mime, 'text/') === 0) || $mime === 'application/json' || $mime === 'application/xml';
            $isTextByExt  = (bool)preg_match('/\.(txt|md|csv|json|xml|html?|css|js|log|ini|conf|config|yml|yaml|php|py|rb|sh|bat|ps1|ts|tsx|c|cpp|h|hpp|java|go|rs)$/i', $file);
            $isText = $isTextByMime || $isTextByExt;

            // Build entry
            $fileEntry = [
                'name'      => $file,
                'modified'  => $fileDateModified,
                'uploaded'  => $fileUploadedDate,
                'size'      => $fileSizeFormatted,
                'sizeBytes' => $fileSizeBytes,            // ← numeric size for frontend logic
                'uploader'  => $fileUploader,
                'tags'      => isset($metadata[$metaKey]['tags']) ? $metadata[$metaKey]['tags'] : [],
                'mime'      => $mime,
            ];

            // Small, safe snippet for text files only (never full content)
            $fileEntry['content']          = '';
            $fileEntry['contentTruncated'] = false;

            if ($isText && $fileSizeBytes > 0) {
                if ($fileSizeBytes <= INDEX_TEXT_BYTES_MAX) {
                    $fh = @fopen($filePath, 'rb');
                    if ($fh) {
                        $snippet = @fread($fh, LISTING_CONTENT_BYTES_MAX);
                        @fclose($fh);
                        if ($snippet !== false) {
                            // ensure UTF-8 for JSON
                            if (function_exists('mb_check_encoding') && !mb_check_encoding($snippet, 'UTF-8')) {
                                if (function_exists('mb_convert_encoding')) {
                                    $snippet = @mb_convert_encoding($snippet, 'UTF-8', 'UTF-8, ISO-8859-1, Windows-1252');
                                }
                            }
                            $fileEntry['content'] = $snippet;
                            $fileEntry['contentTruncated'] = ($fileSizeBytes > LISTING_CONTENT_BYTES_MAX);
                        }
                    }
                } else {
                    // too large to sample: mark truncated so UI/search knows
                    $fileEntry['contentTruncated'] = true;
                }
            }

            $fileList[] = $fileEntry;
        }

        if ($finfo) { @finfo_close($finfo); }

        // Load global tags.
        $globalTagsFile = META_DIR . "createdTags.json";
        $globalTags = file_exists($globalTagsFile) ? (json_decode(file_get_contents($globalTagsFile), true) ?: []) : [];

        return ["files" => $fileList, "globalTags" => $globalTags];
    }

    public static function getAllShareLinks(): array
    {
        $shareFile = META_DIR . "share_links.json";
        if (!file_exists($shareFile)) {
            return [];
        }
        $links = json_decode(file_get_contents($shareFile), true);
        return is_array($links) ? $links : [];
    }

    public static function deleteShareLink(string $token): bool
    {
        $shareFile = META_DIR . "share_links.json";
        if (!file_exists($shareFile)) {
            return false;
        }
        $links = json_decode(file_get_contents($shareFile), true);
        if (!is_array($links) || !isset($links[$token])) {
            return false;
        }
        unset($links[$token]);
        file_put_contents($shareFile, json_encode($links, JSON_PRETTY_PRINT), LOCK_EX);
        return true;
    }

    /**
     * Create an empty file plus metadata entry.
     *
     * @param string $folder
     * @param string $filename
     * @param string $uploader
     * @return array ['success'=>bool, 'error'=>string, 'code'=>int]
     */
    public static function createFile(string $folder, string $filename, string $uploader): array
    {
        // 1) basic validation
        $filename = basename(trim($filename));
        if (!preg_match(REGEX_FILE_NAME, $filename)) {
            return ['success'=>false,'error'=>'Invalid filename','code'=>400];
        }

        // 2) resolve target folder
        list($baseDir, $err) = self::resolveFolderPath($folder, true);
        if ($err) {
            return ['success'=>false, 'error'=>$err, 'code'=>($err === 'Invalid folder name.' ? 400 : 500)];
        }

        $path = $baseDir . DIRECTORY_SEPARATOR . $filename;

        // 3) no overwrite
        if (file_exists($path)) {
            return ['success'=>false,'error'=>'File already exists','code'=>400];
        }

        // 4) touch the file
        if (false === @file_put_contents($path, '', LOCK_EX)) {
            return ['success'=>false,'error'=>'Could not create file','code'=>500];
        }

        // 5) write metadata
        $metaKey  = (strtolower($folder) === 'root' || trim($folder) === '') ? 'root' : $folder;
        $metaName = str_replace(['/', '\\', ' '], '-', $metaKey) . '_metadata.json';
        $metaPath = META_DIR . $metaName;

        $collection = [];
        if (file_exists($metaPath)) {
            $json = file_get_contents($metaPath);
            $collection = json_decode($json, true) ?: [];
        }

        $now = date(DATE_TIME_FORMAT);
        $collection[$filename] = [
          'uploaded' => $now,
          'modified' => $now,
          'uploader' => $uploader
        ];

        if (false === file_put_contents($metaPath, json_encode($collection, JSON_PRETTY_PRINT), LOCK_EX)) {
            return ['success'=>false,'error'=>'Failed to update metadata','code'=>500];
        }

        return ['success'=>true];
    }
}