<?php
declare(strict_types=1);

$secureCookie = !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off';
session_name('clouddrive_admin');
session_set_cookie_params([
    'lifetime' => 0,
    'path' => rtrim(dirname($_SERVER['SCRIPT_NAME'] ?? '/admin/ajax.php'), '/') ?: '/admin',
    'secure' => $secureCookie,
    'httponly' => true,
    'samesite' => 'Lax',
]);
session_start();

header('X-Content-Type-Options: nosniff');

$rootDir = dirname(__DIR__);
$configFile = $rootDir . DIRECTORY_SEPARATOR . 'config.php';
$lockFile = $rootDir . DIRECTORY_SEPARATOR . 'install' . DIRECTORY_SEPARATOR . 'install.lock';

function jsonResponse(bool $success, string $message = '', array $data = [], int $status = 200): void
{
    http_response_code($status);
    header('Content-Type: application/json; charset=utf-8');
    echo json_encode([
        'success' => $success,
        'message' => $message,
        'data' => $data,
    ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
    exit;
}

function requestData(): array
{
    $contentType = $_SERVER['CONTENT_TYPE'] ?? '';
    if (stripos($contentType, 'application/json') !== false) {
        $raw = file_get_contents('php://input');
        $decoded = json_decode($raw ?: '{}', true);
        return is_array($decoded) ? $decoded : [];
    }

    return $_POST + $_GET;
}

function appConfig(string $configFile, string $rootDir): array
{
    if (is_file($configFile)) {
        $config = require $configFile;
        if (is_array($config)) {
            return $config;
        }
    }

    return [
        'installed' => false,
        'database' => [
            'driver' => 'sqlite',
            'path' => $rootDir . DIRECTORY_SEPARATOR . 'data' . DIRECTORY_SEPARATOR . '.clouddrive.sqlite',
        ],
        'upload_path' => $rootDir . DIRECTORY_SEPARATOR . 'uploads',
    ];
}

function pdoConnection(array $config): PDO
{
    $dbPath = $config['database']['path'] ?? '';
    if (($config['database']['driver'] ?? '') !== 'sqlite' || $dbPath === '' || !is_file($dbPath)) {
        jsonResponse(false, '数据文件不可用，请检查安装数据', ['code' => 'DATA_UNAVAILABLE'], 500);
    }

    $pdo = new PDO('sqlite:' . $dbPath);
    $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
    $pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
    $pdo->exec('PRAGMA foreign_keys = ON');
    return $pdo;
}

function currentUser(PDO $pdo): ?array
{
    $userId = (int) ($_SESSION['admin_user_id'] ?? 0);
    if ($userId <= 0) {
        return null;
    }

    $stmt = $pdo->prepare('SELECT id, username, role, status, storage_quota FROM users WHERE id = :id LIMIT 1');
    $stmt->execute([':id' => $userId]);
    $user = $stmt->fetch();
    if (!$user || (int) $user['status'] !== 1 || $user['role'] !== 'admin') {
        unset($_SESSION['admin_user_id']);
        return null;
    }

    return $user;
}

function requireUser(PDO $pdo): array
{
    $user = currentUser($pdo);
    if (!$user) {
        jsonResponse(false, '请先登录', ['code' => 'UNAUTHENTICATED'], 401);
    }

    return $user;
}

function nowText(): string
{
    return date('c');
}

function cleanName(string $name, int $max = 160): string
{
    $name = trim(str_replace(["\0", '/', '\\'], '', $name));
    $name = preg_replace('/\s+/u', ' ', $name) ?: '';
    if (function_exists('mb_substr')) {
        return mb_substr($name, 0, $max, 'UTF-8');
    }

    return substr($name, 0, $max);
}

function normalizeParentId(mixed $value): ?int
{
    if ($value === null || $value === '' || $value === 'root') {
        return null;
    }

    $parentId = (int) $value;
    return $parentId > 0 ? $parentId : null;
}

function ensureParent(PDO $pdo, int $userId, ?int $parentId): void
{
    if ($parentId === null) {
        return;
    }

    $stmt = $pdo->prepare('
        SELECT id FROM files
        WHERE id = :id AND user_id = :user_id AND is_folder = 1 AND is_trashed = 0
        LIMIT 1
    ');
    $stmt->execute([':id' => $parentId, ':user_id' => $userId]);
    if (!$stmt->fetch()) {
        jsonResponse(false, '目标文件夹不存在', [], 404);
    }
}

function siblingFolderNameExists(PDO $pdo, int $userId, ?int $parentId, string $name): bool
{
    $where = [
        'user_id = :user_id',
        'name = :name',
        'is_folder = 1',
        'is_trashed = 0',
    ];
    $params = [
        ':user_id' => $userId,
        ':name' => $name,
    ];

    if ($parentId === null) {
        $where[] = 'parent_id IS NULL';
    } else {
        $where[] = 'parent_id = :parent_id';
        $params[':parent_id'] = $parentId;
    }

    $stmt = $pdo->prepare('SELECT id FROM files WHERE ' . implode(' AND ', $where) . ' LIMIT 1');
    $stmt->execute($params);
    return (bool) $stmt->fetchColumn();
}

function fileTypeFromName(string $name): string
{
    $ext = strtolower(pathinfo($name, PATHINFO_EXTENSION));
    if (in_array($ext, ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'svg'], true)) {
        return 'image';
    }
    if (in_array($ext, ['mp4', 'avi', 'mov', 'mkv', 'webm', 'flv'], true)) {
        return 'video';
    }
    if (in_array($ext, ['mp3', 'wav', 'flac', 'aac', 'ogg'], true)) {
        return 'audio';
    }

    return 'document';
}

function formatFileSize(int $bytes): string
{
    if ($bytes <= 0) {
        return '0 B';
    }
    if ($bytes < 1024) {
        return $bytes . ' B';
    }
    if ($bytes < 1048576) {
        return round($bytes / 1024, 1) . ' KB';
    }
    if ($bytes < 1073741824) {
        return round($bytes / 1048576, 1) . ' MB';
    }
    if ($bytes < 1099511627776) {
        return round($bytes / 1073741824, 1) . ' GB';
    }

    return round($bytes / 1099511627776, 1) . ' TB';
}

function rowToClient(array $row): array
{
    $isFolder = (int) $row['is_folder'] === 1;
    $updatedAt = strtotime((string) $row['updated_at']);

    return [
        'id' => (int) $row['id'],
        'name' => $row['name'],
        'type' => $isFolder ? 'folder' : ($row['type'] ?: 'document'),
        'size' => $isFolder ? '-' : formatFileSize((int) $row['size']),
        'bytes' => (int) $row['size'],
        'date' => $updatedAt ? date('Y-m-d H:i', $updatedAt) : $row['updated_at'],
        'starred' => (int) $row['is_starred'] === 1,
        'shared' => (int) $row['is_shared'] === 1,
        'trashed' => (int) $row['is_trashed'] === 1,
        'path' => $row['parent_id'] === null ? 'root' : (int) $row['parent_id'],
        'ext' => $row['extension'],
    ];
}

function idsFromInput(array $input): array
{
    $ids = $input['ids'] ?? ($input['id'] ?? []);
    if (!is_array($ids)) {
        $ids = [$ids];
    }

    $ids = array_map('intval', $ids);
    $ids = array_values(array_unique(array_filter($ids, fn (int $id): bool => $id > 0)));
    if (!$ids) {
        jsonResponse(false, '请选择文件', [], 422);
    }

    return $ids;
}

function placeholders(array $values, string $prefix): array
{
    $parts = [];
    $params = [];
    foreach (array_values($values) as $index => $value) {
        $key = ':' . $prefix . $index;
        $parts[] = $key;
        $params[$key] = $value;
    }

    return [$parts, $params];
}

function descendantIds(PDO $pdo, int $userId, array $ids): array
{
    [$parts, $params] = placeholders($ids, 'id');
    $sql = '
        WITH RECURSIVE descendants(id) AS (
            SELECT id FROM files WHERE user_id = :user_id AND id IN (' . implode(',', $parts) . ')
            UNION ALL
            SELECT f.id FROM files f
            JOIN descendants d ON f.parent_id = d.id
            WHERE f.user_id = :user_id
        )
        SELECT id FROM descendants
    ';
    $stmt = $pdo->prepare($sql);
    $stmt->execute($params + [':user_id' => $userId]);
    return array_map('intval', array_column($stmt->fetchAll(), 'id'));
}

function existingDiskPath(string $path, string $fallback): string
{
    $probe = $path !== '' ? $path : $fallback;
    while (!is_dir($probe)) {
        $parent = dirname($probe);
        if ($parent === $probe) {
            return $fallback;
        }
        $probe = $parent;
    }

    return $probe;
}

function storageInfo(PDO $pdo, array $user, array $config, string $rootDir): array
{
    $stmt = $pdo->prepare('SELECT COALESCE(SUM(size), 0) AS used FROM files WHERE user_id = :user_id AND is_folder = 0 AND is_trashed = 0');
    $stmt->execute([':user_id' => $user['id']]);
    $used = (int) ($stmt->fetch()['used'] ?? 0);
    $quota = (int) ($user['storage_quota'] ?? 10737418240);
    $uploadRoot = absoluteUploadPath($config, $rootDir);
    $diskPath = existingDiskPath($uploadRoot, $rootDir);
    $free = function_exists('disk_free_space') ? @disk_free_space($diskPath) : false;
    $total = function_exists('disk_total_space') ? @disk_total_space($diskPath) : false;
    $diskFree = $free === false ? null : max(0, (int) $free);
    $diskTotal = $total === false ? null : max(0, (int) $total);
    $diskUsed = $diskFree !== null && $diskTotal !== null ? max(0, $diskTotal - $diskFree) : $used;
    $totalForPercent = $diskTotal !== null && $diskTotal > 0 ? $diskTotal : $quota;
    $usedForPercent = $diskTotal !== null && $diskTotal > 0 ? $diskUsed : $used;
    $remaining = $diskFree ?? max(0, $quota - $used);
    $totalSize = $diskTotal ?? $quota;

    return [
        'used' => $used,
        'quota' => $quota,
        'remaining' => $remaining,
        'total' => $totalSize,
        'disk_used' => $diskUsed,
        'disk_free' => $diskFree,
        'disk_total' => $diskTotal,
        'used_text' => formatFileSize($used),
        'quota_text' => formatFileSize($quota),
        'remaining_text' => formatFileSize($remaining),
        'total_text' => formatFileSize($totalSize),
        'disk_used_text' => formatFileSize($diskUsed),
        'disk_free_text' => $diskFree !== null ? formatFileSize($diskFree) : null,
        'disk_total_text' => $diskTotal !== null ? formatFileSize($diskTotal) : null,
        'percent' => $totalForPercent > 0 ? min(100, round($usedForPercent / $totalForPercent * 100, 1)) : 0,
        'source' => $diskFree !== null && $diskTotal !== null ? 'disk' : 'quota',
    ];
}

function maintenanceStats(PDO $pdo, array $user, array $config): array
{
    $stmt = $pdo->prepare('
        SELECT
            COALESCE(SUM(CASE WHEN is_folder = 0 AND is_trashed = 0 THEN 1 ELSE 0 END), 0) AS active_files,
            COALESCE(SUM(CASE WHEN is_folder = 1 AND is_trashed = 0 THEN 1 ELSE 0 END), 0) AS active_folders,
            COALESCE(SUM(CASE WHEN is_trashed = 1 THEN 1 ELSE 0 END), 0) AS trash_items,
            COALESCE(SUM(CASE WHEN is_folder = 0 AND is_trashed = 1 THEN size ELSE 0 END), 0) AS trash_size,
            COALESCE(SUM(CASE WHEN is_folder = 0 AND is_trashed = 0 THEN size ELSE 0 END), 0) AS used_size,
            COUNT(*) AS total_records
        FROM files
        WHERE user_id = :user_id
    ');
    $stmt->execute([':user_id' => (int) $user['id']]);
    $row = $stmt->fetch() ?: [];

    $dbPath = (string) ($config['database']['path'] ?? '');
    $dbSize = $dbPath !== '' && is_file($dbPath) ? (int) filesize($dbPath) : 0;
    $trashSize = (int) ($row['trash_size'] ?? 0);
    $usedSize = (int) ($row['used_size'] ?? 0);

    return [
        'active_file_count' => (int) ($row['active_files'] ?? 0),
        'active_folder_count' => (int) ($row['active_folders'] ?? 0),
        'trash_item_count' => (int) ($row['trash_items'] ?? 0),
        'trash_size' => $trashSize,
        'trash_size_text' => formatFileSize($trashSize),
        'used_size' => $usedSize,
        'used_size_text' => formatFileSize($usedSize),
        'db_size' => $dbSize,
        'db_size_text' => formatFileSize($dbSize),
        'total_record_count' => (int) ($row['total_records'] ?? 0),
    ];
}

function folderPathInfo(PDO $pdo, int $userId, int $folderId): array
{
    $stmt = $pdo->prepare('
        WITH RECURSIVE ancestors(id, parent_id, name, depth) AS (
            SELECT id, parent_id, name, 0 AS depth
            FROM files
            WHERE user_id = :user_id AND id = :id AND is_folder = 1 AND is_trashed = 0
            UNION ALL
            SELECT f.id, f.parent_id, f.name, a.depth + 1
            FROM files f
            JOIN ancestors a ON a.parent_id = f.id
            WHERE f.user_id = :user_id AND f.is_folder = 1 AND f.is_trashed = 0
        )
        SELECT id, parent_id, name, depth
        FROM ancestors
        ORDER BY depth ASC
    ');
    $stmt->execute([
        ':user_id' => $userId,
        ':id' => $folderId,
    ]);
    $rows = $stmt->fetchAll();
    if (!$rows) {
        jsonResponse(false, '文件夹不存在', ['code' => 'NOT_FOUND'], 404);
    }

    $path = [['id' => 'root', 'name' => '全部文件']];
    foreach (array_reverse($rows) as $row) {
        $path[] = [
            'id' => (string) $row['id'],
            'name' => $row['name'],
        ];
    }

    return $path;
}

function normalizeFilesArray(array $files): array
{
    $normalized = [];
    foreach ($files as $field => $value) {
        if (!is_array($value['name'] ?? null)) {
            $normalized[] = $value;
            continue;
        }

        foreach ($value['name'] as $index => $name) {
            $normalized[] = [
                'name' => $name,
                'type' => $value['type'][$index] ?? '',
                'tmp_name' => $value['tmp_name'][$index] ?? '',
                'error' => $value['error'][$index] ?? UPLOAD_ERR_NO_FILE,
                'size' => $value['size'][$index] ?? 0,
            ];
        }
    }

    return $normalized;
}

function mimeType(string $path): string
{
    if (function_exists('mime_content_type')) {
        return mime_content_type($path) ?: 'application/octet-stream';
    }

    return 'application/octet-stream';
}

function absoluteUploadPath(array $config, string $rootDir): string
{
    $path = (string) ($config['upload_path'] ?? '');
    return $path !== '' ? $path : $rootDir . DIRECTORY_SEPARATOR . 'uploads';
}

function deleteTreeContents(string $dir): void
{
    if (!is_dir($dir)) {
        return;
    }

    $items = scandir($dir);
    if ($items === false) {
        throw new RuntimeException('目录不可读：' . basename($dir));
    }

    foreach ($items as $item) {
        if ($item === '.' || $item === '..') {
            continue;
        }

        $path = $dir . DIRECTORY_SEPARATOR . $item;
        if (is_dir($path) && !is_link($path)) {
            deleteTreeContents($path);
            if (!rmdir($path)) {
                throw new RuntimeException('目录删除失败：' . $item);
            }
            continue;
        }

        if (is_file($path) || is_link($path)) {
            if (!unlink($path)) {
                throw new RuntimeException('文件删除失败：' . $item);
            }
        }
    }
}

function writeInstallDenyFiles(string $dir): void
{
    if (!is_dir($dir) && !mkdir($dir, 0755, true)) {
        throw new RuntimeException('目录创建失败：' . basename($dir));
    }

    $content = <<<HTACCESS
<IfModule mod_authz_core.c>
    Require all denied
</IfModule>
<IfModule !mod_authz_core.c>
    Deny from all
</IfModule>

HTACCESS;

    file_put_contents($dir . DIRECTORY_SEPARATOR . '.htaccess', $content, LOCK_EX);
    file_put_contents($dir . DIRECTORY_SEPARATOR . 'index.html', '', LOCK_EX);
}

function assertRuntimePathInsideRoot(string $path, string $rootDir): void
{
    $root = realpath($rootDir);
    if ($root === false) {
        throw new RuntimeException('项目根目录不可用');
    }

    $target = realpath($path);
    if ($target === false) {
        $parent = realpath(dirname($path));
        if ($parent === false) {
            throw new RuntimeException('运行路径不可用：' . basename($path));
        }
        $target = $parent . DIRECTORY_SEPARATOR . basename($path);
    }

    $root = rtrim($root, DIRECTORY_SEPARATOR);
    $target = rtrim($target, DIRECTORY_SEPARATOR);
    if ($target === $root || strncmp($target . DIRECTORY_SEPARATOR, $root . DIRECTORY_SEPARATOR, strlen($root) + 1) !== 0) {
        throw new RuntimeException('拒绝清理项目目录外的路径：' . basename($path));
    }
}

function restoreFactoryState(array $config, string $rootDir, string $configFile): void
{
    $dataDir = $rootDir . DIRECTORY_SEPARATOR . 'data';
    $uploadDir = absoluteUploadPath($config, $rootDir);
    $dbPath = (string) ($config['database']['path'] ?? ($dataDir . DIRECTORY_SEPARATOR . '.clouddrive.sqlite'));
    $lockFile = $rootDir . DIRECTORY_SEPARATOR . 'install' . DIRECTORY_SEPARATOR . 'install.lock';

    assertRuntimePathInsideRoot($uploadDir, $rootDir);
    assertRuntimePathInsideRoot($dbPath, $rootDir);

    deleteTreeContents($uploadDir);
    if (!is_dir($uploadDir) && !mkdir($uploadDir, 0755, true)) {
        throw new RuntimeException('上传目录重建失败');
    }

    foreach ([$dbPath, $dbPath . '-wal', $dbPath . '-shm', $dbPath . '-journal'] as $file) {
        if (is_file($file) && !unlink($file)) {
            throw new RuntimeException('数据库文件删除失败：' . basename($file));
        }
    }

    foreach ([
        $configFile,
        $lockFile,
    ] as $file) {
        if (is_file($file) && !unlink($file)) {
            throw new RuntimeException('配置文件删除失败：' . basename($file));
        }
    }

    writeInstallDenyFiles($dataDir);
}

try {
    $action = (string) ($_GET['action'] ?? $_POST['action'] ?? '');

    if ($action === '') {
        jsonResponse(false, '未知操作', [], 400);
    }

    if (!is_file($lockFile)) {
        jsonResponse(false, '系统尚未安装，请先完成安装', ['code' => 'NOT_INSTALLED'], 409);
    }

    $config = appConfig($configFile, $rootDir);
    $pdo = pdoConnection($config);

    if ($action === 'status') {
        $user = currentUser($pdo);
        jsonResponse(true, '状态获取成功', [
            'installed' => is_file($lockFile),
            'authenticated' => $user !== null,
            'user' => $user ? [
                'id' => (int) $user['id'],
                'username' => $user['username'],
                'role' => $user['role'],
            ] : null,
        ]);
    }

    if ($action === 'login') {
        if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
            jsonResponse(false, '请使用 POST 请求', [], 405);
        }

        $input = requestData();
        $username = trim((string) ($input['username'] ?? ''));
        $password = (string) ($input['password'] ?? '');

        if ($username === '' || $password === '') {
            jsonResponse(false, '请输入账号和密码', [], 422);
        }

        $stmt = $pdo->prepare('
            SELECT id, username, password_hash, role, status
            FROM users
            WHERE username = :username
            LIMIT 1
        ');
        $stmt->execute([':username' => $username]);
        $user = $stmt->fetch();

        if (!$user || !password_verify($password, (string) $user['password_hash'])) {
            jsonResponse(false, '账号或密码错误', [], 401);
        }
        if ((int) $user['status'] !== 1 || $user['role'] !== 'admin') {
            jsonResponse(false, '账号不可用或不是管理员', [], 403);
        }

        session_regenerate_id(true);
        $_SESSION['admin_user_id'] = (int) $user['id'];
        $stmt = $pdo->prepare('UPDATE users SET last_login_at = :last_login_at, updated_at = :updated_at WHERE id = :id');
        $stmt->execute([
            ':last_login_at' => nowText(),
            ':updated_at' => nowText(),
            ':id' => $user['id'],
        ]);

        jsonResponse(true, '登录成功', [
            'user' => [
                'id' => (int) $user['id'],
                'username' => $user['username'],
                'role' => $user['role'],
            ],
        ]);
    }

    if ($action === 'logout') {
        unset($_SESSION['admin_user_id']);
        session_regenerate_id(true);
        jsonResponse(true, '已退出登录');
    }

    $user = requireUser($pdo);
    $input = requestData();

    if ($action === 'change_password') {
        if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
            jsonResponse(false, '请使用 POST 请求', [], 405);
        }

        $currentPassword = (string) ($input['current_password'] ?? '');
        $newPassword = (string) ($input['new_password'] ?? '');
        $confirmPassword = (string) ($input['confirm_password'] ?? '');

        if ($currentPassword === '' || $newPassword === '' || $confirmPassword === '') {
            jsonResponse(false, '请完整填写密码', [], 422);
        }
        if (strlen($newPassword) < 6) {
            jsonResponse(false, '新密码至少需要 6 位', [], 422);
        }
        if ($newPassword !== $confirmPassword) {
            jsonResponse(false, '两次输入的新密码不一致', [], 422);
        }

        $stmt = $pdo->prepare('SELECT password_hash FROM users WHERE id = :id LIMIT 1');
        $stmt->execute([':id' => (int) $user['id']]);
        $row = $stmt->fetch();
        if (!$row || !password_verify($currentPassword, (string) $row['password_hash'])) {
            jsonResponse(false, '当前密码错误', [], 403);
        }

        $stmt = $pdo->prepare('UPDATE users SET password_hash = :password_hash, updated_at = :updated_at WHERE id = :id');
        $stmt->execute([
            ':password_hash' => password_hash($newPassword, PASSWORD_DEFAULT),
            ':updated_at' => nowText(),
            ':id' => (int) $user['id'],
        ]);
        session_regenerate_id(true);

        jsonResponse(true, '密码修改成功');
    }

    if ($action === 'maintenance_stats') {
        jsonResponse(true, '维护统计获取成功', [
            'stats' => maintenanceStats($pdo, $user, $config),
        ]);
    }

    if ($action === 'empty_trash') {
        if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
            jsonResponse(false, '请使用 POST 请求', [], 405);
        }

        $stmt = $pdo->prepare('SELECT id FROM files WHERE user_id = :user_id AND is_trashed = 1');
        $stmt->execute([':user_id' => (int) $user['id']]);
        $trashIds = array_map('intval', array_column($stmt->fetchAll(), 'id'));

        if (!$trashIds) {
            jsonResponse(true, '回收站已为空', [
                'deleted_count' => 0,
                'stats' => maintenanceStats($pdo, $user, $config),
                'storage' => storageInfo($pdo, $user, $config, $rootDir),
            ]);
        }

        $ids = array_values(array_unique(descendantIds($pdo, (int) $user['id'], $trashIds)));
        [$parts, $params] = placeholders($ids, 'id');

        $stmt = $pdo->prepare('SELECT storage_path FROM files WHERE user_id = :user_id AND is_folder = 0 AND id IN (' . implode(',', $parts) . ')');
        $stmt->execute($params + [':user_id' => (int) $user['id']]);
        foreach ($stmt->fetchAll() as $row) {
            $path = $rootDir . DIRECTORY_SEPARATOR . (string) $row['storage_path'];
            if (is_file($path)) {
                @unlink($path);
            }
        }

        $stmt = $pdo->prepare('DELETE FROM files WHERE user_id = :user_id AND id IN (' . implode(',', $parts) . ')');
        $stmt->execute($params + [':user_id' => (int) $user['id']]);

        jsonResponse(true, '回收站已清空', [
            'deleted_count' => $stmt->rowCount(),
            'stats' => maintenanceStats($pdo, $user, $config),
            'storage' => storageInfo($pdo, $user, $config, $rootDir),
        ]);
    }

    if ($action === 'factory_reset') {
        if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
            jsonResponse(false, '请使用 POST 请求', [], 405);
        }

        $password = (string) ($input['password'] ?? '');
        if ($password === '') {
            jsonResponse(false, '请输入登录密码', [], 422);
        }

        $stmt = $pdo->prepare('SELECT password_hash FROM users WHERE id = :id LIMIT 1');
        $stmt->execute([':id' => (int) $user['id']]);
        $row = $stmt->fetch();
        if (!$row || !password_verify($password, (string) $row['password_hash'])) {
            jsonResponse(false, '登录密码错误', [], 403);
        }

        $_SESSION = [];
        if (session_status() === PHP_SESSION_ACTIVE) {
            session_destroy();
        }
        $pdo = null;

        restoreFactoryState($config, $rootDir, $configFile);

        jsonResponse(true, '已恢复出厂设置', [
            'redirect' => '../install/',
        ]);
    }

    if ($action === 'folder_path') {
        $folderId = (int) ($input['id'] ?? 0);
        if ($folderId <= 0) {
            jsonResponse(false, '参数不完整', [], 422);
        }

        jsonResponse(true, '路径获取成功', [
            'path' => folderPathInfo($pdo, (int) $user['id'], $folderId),
        ]);
    }

    if ($action === 'list') {
        $section = (string) ($input['section'] ?? 'all');
        $type = (string) ($input['type'] ?? '');
        $parentId = normalizeParentId($input['parent_id'] ?? null);

        $where = ['user_id = :user_id'];
        $params = [':user_id' => (int) $user['id']];
        $order = 'is_folder DESC, updated_at DESC';
        $limit = '';

        if ($section === 'trash') {
            $where[] = 'is_trashed = 1';
        } else {
            $where[] = 'is_trashed = 0';
        }

        if ($section === 'starred') {
            $where[] = 'is_starred = 1';
        } elseif ($section === 'shared') {
            $where[] = 'is_shared = 1';
        } elseif ($section === 'recent') {
            $order = 'updated_at DESC';
            $limit = ' LIMIT 100';
        } elseif ($section === 'all') {
            if ($parentId === null) {
                $where[] = 'parent_id IS NULL';
            } else {
                $where[] = 'parent_id = :parent_id';
                $params[':parent_id'] = $parentId;
            }
        }

        if ($type !== '' && in_array($type, ['image', 'document', 'video', 'audio'], true)) {
            $where[] = 'type = :type';
            $params[':type'] = $type;
        }

        $stmt = $pdo->prepare('SELECT * FROM files WHERE ' . implode(' AND ', $where) . ' ORDER BY ' . $order . $limit);
        $stmt->execute($params);
        $rows = $stmt->fetchAll();

        jsonResponse(true, '文件列表获取成功', [
            'files' => array_map('rowToClient', $rows),
            'storage' => storageInfo($pdo, $user, $config, $rootDir),
        ]);
    }

    if ($action === 'create_folder') {
        if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
            jsonResponse(false, '请使用 POST 请求', [], 405);
        }

        $name = cleanName((string) ($input['name'] ?? ''), 120);
        if ($name === '') {
            jsonResponse(false, '请输入文件夹名称', [], 422);
        }

        $parentId = normalizeParentId($input['parent_id'] ?? null);
        ensureParent($pdo, (int) $user['id'], $parentId);
        $now = nowText();

        $transactionStarted = false;
        $pdo->exec('BEGIN IMMEDIATE');
        $transactionStarted = true;
        try {
            if (siblingFolderNameExists($pdo, (int) $user['id'], $parentId, $name)) {
                $pdo->exec('ROLLBACK');
                $transactionStarted = false;
                jsonResponse(false, '当前位置已存在同名文件夹', ['code' => 'DUPLICATE_FOLDER_NAME'], 409);
            }

            $stmt = $pdo->prepare('
                INSERT INTO files(user_id, parent_id, name, type, size, is_folder, created_at, updated_at)
                VALUES(:user_id, :parent_id, :name, "folder", 0, 1, :created_at, :updated_at)
            ');
            $stmt->execute([
                ':user_id' => (int) $user['id'],
                ':parent_id' => $parentId,
                ':name' => $name,
                ':created_at' => $now,
                ':updated_at' => $now,
            ]);

            $id = (int) $pdo->lastInsertId();
            $row = $pdo->query('SELECT * FROM files WHERE id = ' . $id)->fetch();
            $file = rowToClient($row);
            $pdo->exec('COMMIT');
            $transactionStarted = false;
        } catch (Throwable $e) {
            if ($transactionStarted) {
                $pdo->exec('ROLLBACK');
            }
            throw $e;
        }

        jsonResponse(true, '文件夹创建成功', ['file' => $file]);
    }

    if ($action === 'upload') {
        if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
            jsonResponse(false, '请使用 POST 请求', [], 405);
        }

        $parentId = normalizeParentId($input['parent_id'] ?? null);
        ensureParent($pdo, (int) $user['id'], $parentId);

        $uploads = normalizeFilesArray($_FILES);
        if (!$uploads) {
            jsonResponse(false, '请选择要上传的文件', [], 422);
        }

        $uploadRoot = absoluteUploadPath($config, $rootDir);
        $monthDir = date('Ym');
        $targetDir = $uploadRoot . DIRECTORY_SEPARATOR . (int) $user['id'] . DIRECTORY_SEPARATOR . $monthDir;
        if (!is_dir($targetDir) && !mkdir($targetDir, 0755, true)) {
            jsonResponse(false, '上传目录创建失败', [], 500);
        }

        $created = [];
        $now = nowText();
        foreach ($uploads as $file) {
            if ((int) ($file['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) {
                continue;
            }

            $originalName = cleanName((string) $file['name']);
            if ($originalName === '') {
                $originalName = 'upload-' . date('YmdHis');
            }

            $extension = strtolower(pathinfo($originalName, PATHINFO_EXTENSION));
            $storedName = bin2hex(random_bytes(16)) . ($extension ? '.' . $extension : '');
            $absolutePath = $targetDir . DIRECTORY_SEPARATOR . $storedName;
            $tmpName = (string) $file['tmp_name'];

            if (!is_uploaded_file($tmpName) || !move_uploaded_file($tmpName, $absolutePath)) {
                continue;
            }

            $relativePath = 'uploads/' . (int) $user['id'] . '/' . $monthDir . '/' . $storedName;
            $stmt = $pdo->prepare('
                INSERT INTO files(user_id, parent_id, name, type, extension, mime_type, storage_path, size, is_folder, created_at, updated_at)
                VALUES(:user_id, :parent_id, :name, :type, :extension, :mime_type, :storage_path, :size, 0, :created_at, :updated_at)
            ');
            $stmt->execute([
                ':user_id' => (int) $user['id'],
                ':parent_id' => $parentId,
                ':name' => $originalName,
                ':type' => fileTypeFromName($originalName),
                ':extension' => $extension,
                ':mime_type' => mimeType($absolutePath),
                ':storage_path' => $relativePath,
                ':size' => (int) $file['size'],
                ':created_at' => $now,
                ':updated_at' => $now,
            ]);

            $id = (int) $pdo->lastInsertId();
            $row = $pdo->query('SELECT * FROM files WHERE id = ' . $id)->fetch();
            $created[] = rowToClient($row);
        }

        if (!$created) {
            jsonResponse(false, '文件上传失败', [], 500);
        }

        jsonResponse(true, '上传成功', [
            'files' => $created,
            'storage' => storageInfo($pdo, $user, $config, $rootDir),
        ]);
    }

    if ($action === 'rename') {
        $id = (int) ($input['id'] ?? 0);
        $name = cleanName((string) ($input['name'] ?? ''));
        if ($id <= 0 || $name === '') {
            jsonResponse(false, '参数不完整', [], 422);
        }

        $stmt = $pdo->prepare('UPDATE files SET name = :name, updated_at = :updated_at WHERE id = :id AND user_id = :user_id');
        $stmt->execute([
            ':name' => $name,
            ':updated_at' => nowText(),
            ':id' => $id,
            ':user_id' => (int) $user['id'],
        ]);

        jsonResponse(true, '重命名成功');
    }

    if ($action === 'toggle_star') {
        $id = (int) ($input['id'] ?? 0);
        if ($id <= 0) {
            jsonResponse(false, '参数不完整', [], 422);
        }

        $stmt = $pdo->prepare('
            UPDATE files
            SET is_starred = CASE WHEN is_starred = 1 THEN 0 ELSE 1 END,
                updated_at = :updated_at
            WHERE id = :id AND user_id = :user_id
        ');
        $stmt->execute([
            ':updated_at' => nowText(),
            ':id' => $id,
            ':user_id' => (int) $user['id'],
        ]);

        $stmt = $pdo->prepare('SELECT is_starred FROM files WHERE id = :id AND user_id = :user_id');
        $stmt->execute([':id' => $id, ':user_id' => (int) $user['id']]);
        $row = $stmt->fetch();
        jsonResponse(true, '收藏状态已更新', ['starred' => $row ? (int) $row['is_starred'] === 1 : false]);
    }

    if ($action === 'delete') {
        $ids = descendantIds($pdo, (int) $user['id'], idsFromInput($input));
        if (!$ids) {
            jsonResponse(false, '文件不存在', [], 404);
        }

        [$parts, $params] = placeholders($ids, 'id');
        $stmt = $pdo->prepare('
            UPDATE files
            SET is_trashed = 1, deleted_at = :deleted_at, updated_at = :updated_at
            WHERE user_id = :user_id AND id IN (' . implode(',', $parts) . ')
        ');
        $stmt->execute($params + [
            ':deleted_at' => nowText(),
            ':updated_at' => nowText(),
            ':user_id' => (int) $user['id'],
        ]);

        jsonResponse(true, '已移至回收站', ['ids' => $ids]);
    }

    if ($action === 'restore') {
        $ids = idsFromInput($input);
        [$parts, $params] = placeholders($ids, 'id');
        $stmt = $pdo->prepare('
            UPDATE files
            SET is_trashed = 0, deleted_at = NULL, updated_at = :updated_at
            WHERE user_id = :user_id AND id IN (' . implode(',', $parts) . ')
        ');
        $stmt->execute($params + [
            ':updated_at' => nowText(),
            ':user_id' => (int) $user['id'],
        ]);
        jsonResponse(true, '已恢复');
    }

    if ($action === 'destroy') {
        $ids = descendantIds($pdo, (int) $user['id'], idsFromInput($input));
        if (!$ids) {
            jsonResponse(false, '文件不存在', [], 404);
        }

        [$parts, $params] = placeholders($ids, 'id');
        $stmt = $pdo->prepare('SELECT storage_path FROM files WHERE user_id = :user_id AND is_folder = 0 AND id IN (' . implode(',', $parts) . ')');
        $stmt->execute($params + [':user_id' => (int) $user['id']]);
        foreach ($stmt->fetchAll() as $row) {
            $path = $rootDir . DIRECTORY_SEPARATOR . (string) $row['storage_path'];
            if (is_file($path)) {
                @unlink($path);
            }
        }

        $stmt = $pdo->prepare('DELETE FROM files WHERE user_id = :user_id AND id IN (' . implode(',', $parts) . ')');
        $stmt->execute($params + [':user_id' => (int) $user['id']]);
        jsonResponse(true, '已永久删除');
    }

    if ($action === 'share') {
        $id = (int) ($input['id'] ?? 0);
        if ($id <= 0) {
            jsonResponse(false, '参数不完整', [], 422);
        }

        $stmt = $pdo->prepare('SELECT id FROM files WHERE id = :id AND user_id = :user_id AND is_trashed = 0 LIMIT 1');
        $stmt->execute([':id' => $id, ':user_id' => (int) $user['id']]);
        if (!$stmt->fetch()) {
            jsonResponse(false, '文件不存在', [], 404);
        }

        $token = bin2hex(random_bytes(16));
        $stmt = $pdo->prepare('INSERT INTO shares(file_id, token, created_at) VALUES(:file_id, :token, :created_at)');
        $stmt->execute([
            ':file_id' => $id,
            ':token' => $token,
            ':created_at' => nowText(),
        ]);
        $pdo->prepare('UPDATE files SET is_shared = 1, updated_at = :updated_at WHERE id = :id')->execute([
            ':updated_at' => nowText(),
            ':id' => $id,
        ]);

        $scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
        $host = $_SERVER['HTTP_HOST'] ?? '';
        $link = $host ? $scheme . '://' . $host . '/share.php?token=' . $token : '/share.php?token=' . $token;
        jsonResponse(true, '分享链接已生成', ['token' => $token, 'link' => $link]);
    }

    if ($action === 'download') {
        $id = (int) ($input['id'] ?? 0);
        if ($id <= 0) {
            jsonResponse(false, '参数不完整', [], 422);
        }

        $stmt = $pdo->prepare('
            SELECT name, mime_type, storage_path, size
            FROM files
            WHERE id = :id AND user_id = :user_id AND is_folder = 0 AND is_trashed = 0
            LIMIT 1
        ');
        $stmt->execute([':id' => $id, ':user_id' => (int) $user['id']]);
        $file = $stmt->fetch();
        if (!$file) {
            jsonResponse(false, '文件不存在', [], 404);
        }

        $path = $rootDir . DIRECTORY_SEPARATOR . (string) $file['storage_path'];
        if (!is_file($path)) {
            jsonResponse(false, '文件实体不存在', [], 404);
        }

        session_write_close();
        header('Content-Type: ' . ($file['mime_type'] ?: 'application/octet-stream'));
        header('Content-Length: ' . (string) filesize($path));
        header("Content-Disposition: attachment; filename*=UTF-8''" . rawurlencode((string) $file['name']));
        readfile($path);
        exit;
    }

    jsonResponse(false, '未知操作', [], 400);
} catch (Throwable $e) {
    jsonResponse(false, '请求失败：' . $e->getMessage(), [], 500);
}
