const {
  S3Client,
  PutObjectCommand,
  GetObjectCommand,
  DeleteObjectsCommand,
  ListObjectsV2Command,
  HeadObjectCommand,
  CopyObjectCommand,
  CreateMultipartUploadCommand,
  UploadPartCommand,
  CompleteMultipartUploadCommand,
  AbortMultipartUploadCommand
} = require('@aws-sdk/client-s3');
const { getSignedUrl } = require('@aws-sdk/s3-request-presigner');
const fs = require('fs');
const path = require('path');
const { Readable } = require('stream');
const { UserDB, SettingsDB } = require('./database');

// ===== 工具函数 =====

/**
 * 格式化文件大小
 */
function formatFileSize(bytes) {
  if (bytes === 0) return '0 B';
  const k = 1024;
  const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
  const i = Math.floor(Math.log(bytes) / Math.log(k));
  return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
}

function parsePositiveInteger(value, fallback) {
  const parsed = Number(value);
  if (!Number.isFinite(parsed) || parsed <= 0) {
    return fallback;
  }
  return Math.floor(parsed);
}

const OSS_MULTIPART_MAX_PARTS = 10000;
const OSS_MULTIPART_MIN_PART_SIZE = 5 * 1024 * 1024; // S3 协议要求最小 5MB（最后一片除外）
const OSS_MULTIPART_DEFAULT_PART_SIZE = Math.max(
  OSS_MULTIPART_MIN_PART_SIZE,
  parsePositiveInteger(process.env.OSS_MULTIPART_PART_SIZE_BYTES, 16 * 1024 * 1024)
);
const OSS_SINGLE_UPLOAD_THRESHOLD = Math.max(
  OSS_MULTIPART_MIN_PART_SIZE,
  parsePositiveInteger(process.env.OSS_SINGLE_UPLOAD_THRESHOLD_BYTES, 64 * 1024 * 1024)
);

/**
 * 将 OSS/网络错误转换为友好的错误信息
 * @param {Error} error - 原始错误
 * @param {string} operation - 操作描述
 * @returns {Error} 带有友好消息的错误
 */
function formatOssError(error, operation = '操作') {
  // 常见的 AWS S3 / OSS 错误
  const errorMessages = {
    'NoSuchBucket': 'OSS 存储桶不存在，请检查配置',
    'AccessDenied': 'OSS 访问被拒绝，请检查权限配置',
    'InvalidAccessKeyId': 'OSS Access Key 无效，请重新配置',
    'SignatureDoesNotMatch': 'OSS 签名验证失败，请检查 Secret Key',
    'NoSuchKey': '文件或目录不存在',
    'EntityTooLarge': '文件过大，超过了 OSS 允许的最大大小',
    'RequestTimeout': 'OSS 请求超时，请稍后重试',
    'SlowDown': 'OSS 请求过于频繁，请稍后重试',
    'ServiceUnavailable': 'OSS 服务暂时不可用，请稍后重试',
    'InternalError': 'OSS 内部错误，请稍后重试',
    'BucketNotEmpty': '存储桶不为空',
    'InvalidBucketName': '无效的存储桶名称',
    'InvalidObjectName': '无效的对象名称',
    'TooManyBuckets': '存储桶数量超过限制'
  };

  // 网络错误
  const networkErrors = {
    'ECONNREFUSED': '无法连接到 OSS 服务，请检查网络',
    'ENOTFOUND': 'OSS 服务地址无法解析，请检查 endpoint 配置',
    'ETIMEDOUT': '连接 OSS 服务超时，请检查网络',
    'ECONNRESET': '与 OSS 服务的连接被重置，请重试',
    'EPIPE': '与 OSS 服务的连接中断，请重试',
    'EHOSTUNREACH': '无法访问 OSS 服务主机，请检查网络'
  };

  // 检查 AWS SDK 错误名称
  if (error.name && errorMessages[error.name]) {
    return new Error(`${operation}失败: ${errorMessages[error.name]}`);
  }

  // 检查网络错误代码
  if (error.code && networkErrors[error.code]) {
    return new Error(`${operation}失败: ${networkErrors[error.code]}`);
  }

  // HTTP 状态码错误
  if (error.$metadata?.httpStatusCode) {
    const statusCode = error.$metadata.httpStatusCode;
    const statusMessages = {
      400: '请求参数错误',
      401: '认证失败，请检查 Access Key',
      403: '没有权限执行此操作',
      404: '资源不存在',
      409: '资源冲突',
      429: '请求过于频繁，请稍后重试',
      500: 'OSS 服务内部错误',
      502: 'OSS 网关错误',
      503: 'OSS 服务暂时不可用'
    };
    if (statusMessages[statusCode]) {
      return new Error(`${operation}失败: ${statusMessages[statusCode]}`);
    }
  }

  // 返回原始错误信息
  return new Error(`${operation}失败: ${error.message}`);
}

// ===== 统一存储接口 =====

/**
 * 存储接口工厂
 * 根据用户的存储类型返回对应的存储客户端
 */
class StorageInterface {
  constructor(user) {
    this.user = user;
    this.type = user.current_storage_type || 'oss';
  }

  /**
   * 创建并返回存储客户端
   */
  async connect() {
    if (this.type === 'local') {
      const client = new LocalStorageClient(this.user);
      await client.init();
      return client;
    } else {
      // OSS 客户端会自动检查是否有可用配置（系统配置或用户配置）
      // 不再在这里强制检查 has_oss_config
      const client = new OssStorageClient(this.user);
      await client.connect();
      return client;
    }
  }
}

// ===== 本地存储客户端 =====

class LocalStorageClient {
  constructor(user) {
    this.user = user;
    // 使用环境变量或默认路径（不硬编码）
    const storageRoot = process.env.STORAGE_ROOT || path.join(__dirname, 'storage');
    this.basePath = path.join(storageRoot, `user_${user.id}`);
  }

  /**
   * 初始化用户存储目录
   */
  async init() {
    if (!fs.existsSync(this.basePath)) {
      fs.mkdirSync(this.basePath, { recursive: true, mode: 0o755 });
      console.log(`[本地存储] 创建用户目录: ${this.basePath}`);
    }
  }

  /**
   * 列出目录内容
   * @param {string} dirPath - 目录路径
   * @returns {Promise<Array>} 文件列表
   */
  async list(dirPath) {
    const fullPath = this.getFullPath(dirPath);

    // 确保目录存在
    if (!fs.existsSync(fullPath)) {
      fs.mkdirSync(fullPath, { recursive: true });
      return [];
    }

    // 检查是否是目录
    const pathStats = fs.statSync(fullPath);
    if (!pathStats.isDirectory()) {
      throw new Error('指定路径不是目录');
    }

    const items = fs.readdirSync(fullPath, { withFileTypes: true });
    const result = [];

    for (const item of items) {
      try {
        const itemPath = path.join(fullPath, item.name);
        const stats = fs.statSync(itemPath);
        result.push({
          name: item.name,
          type: item.isDirectory() ? 'd' : '-',
          size: stats.size,
          modifyTime: stats.mtimeMs
        });
      } catch (error) {
        // 跳过无法访问的文件（权限问题或符号链接断裂等）
        console.warn(`[本地存储] 无法获取文件信息，跳过: ${item.name}`, error.message);
      }
    }

    return result;
  }

  /**
   * 上传文件
   */
  async put(localPath, remotePath) {
    const destPath = this.getFullPath(remotePath);

    // 获取新文件大小
    const newFileSize = fs.statSync(localPath).size;

    // 如果目标文件存在，计算实际需要的额外空间
    let oldFileSize = 0;
    if (fs.existsSync(destPath)) {
      try {
        oldFileSize = fs.statSync(destPath).size;
      } catch (err) {
        // 文件可能已被删除，忽略错误
      }
    }

    // 检查配额：只检查净增量（新文件大小 - 旧文件大小）
    const netIncrease = newFileSize - oldFileSize;
    if (netIncrease > 0) {
      this.checkQuota(netIncrease);
    }

    // 确保目标目录存在
    const destDir = path.dirname(destPath);
    if (!fs.existsSync(destDir)) {
      fs.mkdirSync(destDir, { recursive: true });
    }

    // 使用临时文件+重命名模式，避免文件被占用问题
    const tempPath = `${destPath}.uploading_${Date.now()}`;

    try {
      // 复制到临时文件
      fs.copyFileSync(localPath, tempPath);

      // 如果目标文件存在，先删除
      if (fs.existsSync(destPath)) {
        fs.unlinkSync(destPath);
      }

      // 重命名临时文件为目标文件
      fs.renameSync(tempPath, destPath);

      // 更新已使用空间（使用净增量）
      if (netIncrease !== 0) {
        this.updateUsedSpace(netIncrease);
      }
    } catch (error) {
      // 清理临时文件
      if (fs.existsSync(tempPath)) {
        fs.unlinkSync(tempPath);
      }
      throw error;
    }
  }

  /**
   * 删除文件或文件夹
   */
  async delete(filePath) {
    const fullPath = this.getFullPath(filePath);

    // 检查文件是否存在
    if (!fs.existsSync(fullPath)) {
      console.warn(`[本地存储] 删除目标不存在，跳过: ${filePath}`);
      return; // 文件不存在，直接返回（幂等操作）
    }

    let stats;
    try {
      stats = fs.statSync(fullPath);
    } catch (error) {
      if (error.code === 'ENOENT') {
        // 文件在检查后被删除，直接返回
        return;
      }
      throw error;
    }

    if (stats.isDirectory()) {
      // 删除文件夹 - 递归删除
      // 先计算文件夹内所有文件的总大小
      const folderSize = this.calculateFolderSize(fullPath);

      // 删除文件夹及其内容
      fs.rmSync(fullPath, { recursive: true, force: true });

      // 更新已使用空间
      if (folderSize > 0) {
        this.updateUsedSpace(-folderSize);
      }
      console.log(`[本地存储] 删除文件夹: ${filePath} (释放 ${this.formatSize(folderSize)})`);
    } else {
      const fileSize = stats.size;
      // 删除文件
      fs.unlinkSync(fullPath);

      // 更新已使用空间
      this.updateUsedSpace(-fileSize);
      console.log(`[本地存储] 删除文件: ${filePath} (释放 ${this.formatSize(fileSize)})`);
    }
  }

  /**
   * 计算文件夹大小
   */
  calculateFolderSize(folderPath) {
    let totalSize = 0;

    const items = fs.readdirSync(folderPath, { withFileTypes: true });

    for (const item of items) {
      const itemPath = path.join(folderPath, item.name);

      if (item.isDirectory()) {
        // 递归计算子文件夹
        totalSize += this.calculateFolderSize(itemPath);
      } else {
        // 累加文件大小
        const stats = fs.statSync(itemPath);
        totalSize += stats.size;
      }
    }

    return totalSize;
  }

  /**
   * 重命名文件或目录
   * @param {string} oldPath - 原路径
   * @param {string} newPath - 新路径
   */
  async rename(oldPath, newPath) {
    const oldFullPath = this.getFullPath(oldPath);
    const newFullPath = this.getFullPath(newPath);

    // 检查源和目标是否相同
    if (oldFullPath === newFullPath) {
      console.log(`[本地存储] 源路径和目标路径相同，跳过: ${oldPath}`);
      return;
    }

    // 检查源文件是否存在
    if (!fs.existsSync(oldFullPath)) {
      throw new Error('源文件或目录不存在');
    }

    // 检查目标是否已存在（防止覆盖）
    if (fs.existsSync(newFullPath)) {
      throw new Error('目标位置已存在同名文件或目录');
    }

    // 确保新路径的目录存在
    const newDir = path.dirname(newFullPath);
    if (!fs.existsSync(newDir)) {
      fs.mkdirSync(newDir, { recursive: true });
    }

    fs.renameSync(oldFullPath, newFullPath);
    console.log(`[本地存储] 重命名: ${oldPath} -> ${newPath}`);
  }

  /**
   * 获取文件信息
   * @param {string} filePath - 文件路径
   * @returns {Promise<Object>} 文件状态信息，包含 isDirectory 属性
   */
  async stat(filePath) {
    const fullPath = this.getFullPath(filePath);

    if (!fs.existsSync(fullPath)) {
      throw new Error(`文件或目录不存在: ${filePath}`);
    }

    const stats = fs.statSync(fullPath);
    // 返回与 OssStorageClient.stat 一致的格式
    return {
      size: stats.size,
      modifyTime: stats.mtimeMs,
      isDirectory: stats.isDirectory(),
      // 保留原始 stats 对象的方法兼容性
      isFile: () => stats.isFile(),
      _raw: stats
    };
  }

  /**
   * 创建文件读取流
   * @param {string} filePath - 文件路径
   * @returns {ReadStream} 文件读取流
   */
  createReadStream(filePath) {
    const fullPath = this.getFullPath(filePath);

    if (!fs.existsSync(fullPath)) {
      throw new Error(`文件不存在: ${filePath}`);
    }

    return fs.createReadStream(fullPath);
  }

  /**
   * 创建文件夹
   * @param {string} dirPath - 目录路径
   */
  async mkdir(dirPath) {
    const fullPath = this.getFullPath(dirPath);

    // 检查是否已存在
    if (fs.existsSync(fullPath)) {
      const stats = fs.statSync(fullPath);
      if (stats.isDirectory()) {
        // 目录已存在，直接返回
        return;
      }
      throw new Error('同名文件已存在');
    }

    // 创建目录
    fs.mkdirSync(fullPath, { recursive: true, mode: 0o755 });
    console.log(`[本地存储] 创建文件夹: ${dirPath}`);
  }

  /**
   * 检查文件或目录是否存在
   * @param {string} filePath - 文件路径
   * @returns {Promise<boolean>}
   */
  async exists(filePath) {
    try {
      const fullPath = this.getFullPath(filePath);
      return fs.existsSync(fullPath);
    } catch (error) {
      return false;
    }
  }

  /**
   * 关闭连接（本地存储无需关闭）
   */
  async end() {
    // 本地存储无需关闭连接
  }

  // ===== 辅助方法 =====

  /**
   * 获取完整路径（带安全检查）
   * 增强的路径遍历防护
   */
  getFullPath(relativePath) {
    // 0. 输入验证：检查空字节注入和其他危险字符
    if (typeof relativePath !== 'string') {
      throw new Error('无效的路径类型');
    }

    // 检查空字节注入（%00, \x00）
    if (relativePath.includes('\x00') || relativePath.includes('%00')) {
      console.warn('[安全] 检测到空字节注入尝试:', relativePath);
      throw new Error('路径包含非法字符');
    }

    // 1. 规范化路径，移除 ../ 等危险路径
    let normalized = path.normalize(relativePath || '').replace(/^(\.\.[\/\\])+/, '');

    // 2. 额外检查：移除路径中间的 .. （防止 a/../../../etc/passwd 绕过）
    // 解析后的路径不应包含 ..
    if (normalized.includes('..')) {
      console.warn('[安全] 检测到目录遍历尝试:', relativePath);
      throw new Error('路径包含非法字符');
    }

    // 3. 将绝对路径转换为相对路径（解决Linux环境下的问题）
    if (path.isAbsolute(normalized)) {
      // 移除开头的 / 或 Windows 盘符，转为相对路径
      normalized = normalized.replace(/^[\/\\]+/, '').replace(/^[a-zA-Z]:/, '');
    }

    // 4. 空字符串或 . 表示根目录
    if (normalized === '' || normalized === '.') {
      return this.basePath;
    }

    // 5. 拼接完整路径
    const fullPath = path.join(this.basePath, normalized);

    // 6. 解析真实路径（处理符号链接）后再次验证
    const resolvedBasePath = path.resolve(this.basePath);
    const resolvedFullPath = path.resolve(fullPath);

    // 7. 安全检查：确保路径在用户目录内（防止目录遍历攻击）
    if (!resolvedFullPath.startsWith(resolvedBasePath)) {
      console.warn('[安全] 检测到路径遍历攻击:', {
        input: relativePath,
        resolved: resolvedFullPath,
        base: resolvedBasePath
      });
      throw new Error('非法路径访问');
    }

    return fullPath;
  }

  /**
   * 检查配额
   */
  checkQuota(additionalSize) {
    const newUsed = (this.user.local_storage_used || 0) + additionalSize;
    if (newUsed > this.user.local_storage_quota) {
      const used = this.formatSize(this.user.local_storage_used);
      const quota = this.formatSize(this.user.local_storage_quota);
      const need = this.formatSize(additionalSize);
      throw new Error(`存储配额不足。已使用: ${used}, 配额: ${quota}, 需要: ${need}`);
    }
  }

  /**
   * 更新已使用空间
   */
  updateUsedSpace(delta) {
    const newUsed = Math.max(0, (this.user.local_storage_used || 0) + delta);
    UserDB.update(this.user.id, { local_storage_used: newUsed });
    // 更新内存中的值
    this.user.local_storage_used = newUsed;
  }

  /**
   * 恢复未完成的重命名操作（启动时调用）
   * 扫描OSS存储中的待处理重命名标记文件，执行回滚或完成操作
   *
   * **重命名操作的两个阶段：**
   * 1. copying 阶段：正在复制文件到新位置
   *    - 恢复策略：删除已复制的目标文件，保留原文件
   * 2. deleting 阶段：正在删除原文件
   *    - 恢复策略：确保原文件被完全删除（补充删除逻辑）
   *
   * @private
   */
  async recoverPendingRenames() {
    try {
      console.log('[OSS存储] 检查未完成的重命名操作...');

      const bucket = this.getBucket();
      const markerPrefix = this.prefix + '.rename_pending_';

      // 列出所有待处理的标记文件
      const listCommand = new ListObjectsV2Command({
        Bucket: bucket,
        Prefix: markerPrefix,
        MaxKeys: 100
      });

      const response = await this.s3Client.send(listCommand);

      if (!response.Contents || response.Contents.length === 0) {
        console.log('[OSS存储] 没有未完成的重命名操作');
        return;
      }

      console.log(`[OSS存储] 发现 ${response.Contents.length} 个未完成的重命名操作，开始恢复...`);

      for (const marker of response.Contents) {
        try {
          // 从标记文件名中解析元数据
          // 格式: .rename_pending_{timestamp}_{oldKeyHash}.json
          const markerKey = marker.Key;

          // 读取标记文件内容
          const getMarkerCommand = new GetObjectCommand({
            Bucket: bucket,
            Key: markerKey
          });

          const markerResponse = await this.s3Client.send(getMarkerCommand);
          const markerContent = await streamToBuffer(markerResponse.Body);
          const metadata = JSON.parse(markerContent.toString());

          const { oldPrefix, newPrefix, timestamp, phase } = metadata;

          // 检查标记是否过期（超过1小时视为失败，需要恢复）
          const age = Date.now() - timestamp;
          const TIMEOUT = 60 * 60 * 1000; // 1小时

          if (age > TIMEOUT) {
            console.warn(`[OSS存储] 检测到超时的重命名操作: ${oldPrefix} -> ${newPrefix}, 阶段: ${phase}`);

            // 根据不同阶段执行不同的恢复策略
            if (phase === 'copying') {
              // ===== 第一阶段：复制阶段超时 =====
              // 策略：删除已复制的目标文件，保留原文件
              console.log(`[OSS存储] [copying阶段] 执行回滚: 删除已复制的文件 ${newPrefix}`);
              await this._rollbackRename(oldPrefix, newPrefix);

            } else if (phase === 'deleting') {
              // ===== 第二阶段：删除阶段超时（第二轮修复） =====
              // 策略：补充完整的删除逻辑，确保原文件被清理干净
              console.log(`[OSS存储] [deleting阶段] 执行补充删除: 清理剩余原文件 ${oldPrefix}`);

              try {
                // 步骤1：列出原位置的所有剩余文件
                let continuationToken = null;
                let remainingCount = 0;
                const MAX_KEYS_PER_REQUEST = 1000;

                do {
                  const listOldCommand = new ListObjectsV2Command({
                    Bucket: bucket,
                    Prefix: oldPrefix,
                    MaxKeys: MAX_KEYS_PER_REQUEST,
                    ContinuationToken: continuationToken
                  });

                  const listOldResponse = await this.s3Client.send(listOldCommand);
                  continuationToken = listOldResponse.NextContinuationToken;

                  if (listOldResponse.Contents && listOldResponse.Contents.length > 0) {
                    // 步骤2：批量删除剩余的原文件
                    const deleteCommand = new DeleteObjectsCommand({
                      Bucket: bucket,
                      Delete: {
                        Objects: listOldResponse.Contents.map(obj => ({ Key: obj.Key })),
                        Quiet: true
                      }
                    });

                    const deleteResult = await this.s3Client.send(deleteCommand);
                    remainingCount += listOldResponse.Contents.length;

                    console.log(`[OSS存储] [deleting阶段] 已删除 ${listOldResponse.Contents.length} 个剩余原文件`);

                    // 检查删除结果
                    if (deleteResult.Errors && deleteResult.Errors.length > 0) {
                      console.warn(`[OSS存储] [deleting阶段] 部分文件删除失败:`, deleteResult.Errors);
                    }
                  }
                } while (continuationToken);

                if (remainingCount > 0) {
                  console.log(`[OSS存储] [deleting阶段] 补充删除完成: 清理了 ${remainingCount} 个原文件`);
                } else {
                  console.log(`[OSS存储] [deleting阶段] 原位置 ${oldPrefix} 已是空的，无需清理`);
                }

              } catch (cleanupError) {
                console.error(`[OSS存储] [deleting阶段] 补充删除失败: ${cleanupError.message}`);
                // 继续执行，不中断流程
              }

            } else {
              // 未知阶段，记录警告
              console.warn(`[OSS存储] 未知阶段 ${phase}，跳过恢复`);
            }

            // 删除标记文件（完成恢复后清理）
            await this.s3Client.send(new DeleteObjectsCommand({
              Bucket: bucket,
              Delete: {
                Objects: [{ Key: markerKey }],
                Quiet: true
              }
            }));

            console.log(`[OSS存储] 已清理超时的重命名标记: ${markerKey}`);
          } else {
            console.log(`[OSS存储] 重命名操作仍在进行中: ${oldPrefix} -> ${newPrefix} (阶段: ${phase}, 剩余: ${Math.floor((TIMEOUT - age) / 1000)}秒)`);
          }
        } catch (error) {
          console.error(`[OSS存储] 恢复重命名操作失败: ${marker.Key}`, error.message);
          // 继续处理下一个标记文件
        }
      }

      console.log('[OSS存储] 重命名操作恢复完成');
    } catch (error) {
      console.error('[OSS存储] 恢复重命名操作时出错:', error.message);
    }
  }

  /**
   * 回滚重命名操作（删除已复制的目标文件）
   * @param {string} oldPrefix - 原前缀
   * @param {string} newPrefix - 新前缀
   * @private
   */
  async _rollbackRename(oldPrefix, newPrefix) {
    const bucket = this.getBucket();
    const newPrefixWithSlash = newPrefix.endsWith('/') ? newPrefix : `${newPrefix}/`;

    try {
      // 列出所有已复制的对象
      let continuationToken = null;
      let deletedCount = 0;

      do {
        const listCommand = new ListObjectsV2Command({
          Bucket: bucket,
          Prefix: newPrefixWithSlash,
          ContinuationToken: continuationToken,
          MaxKeys: 1000
        });

        const listResponse = await this.s3Client.send(listCommand);
        continuationToken = listResponse.NextContinuationToken;

        if (listResponse.Contents && listResponse.Contents.length > 0) {
          // 批量删除
          const deleteCommand = new DeleteObjectsCommand({
            Bucket: bucket,
            Delete: {
              Objects: listResponse.Contents.map(obj => ({ Key: obj.Key })),
              Quiet: true
            }
          });

          await this.s3Client.send(deleteCommand);
          deletedCount += listResponse.Contents.length;
        }
      } while (continuationToken);

      if (deletedCount > 0) {
        console.log(`[OSS存储] 回滚完成: 删除了 ${deletedCount} 个对象`);
      }
    } catch (error) {
      console.error(`[OSS存储] 回滚失败: ${error.message}`);
      throw error;
    }
  }

  /**
   * 格式化文件大小
   */
  formatSize(bytes) {
    return formatFileSize(bytes);
  }
}

// ===== OSS存储客户端 =====

/**
 * OSS 存储客户端（基于 S3 协议）
 * 支持阿里云 OSS、腾讯云 COS、AWS S3
 *
 * **优先级规则：**
 * 1. 如果系统配置了统一 OSS（管理员配置），优先使用系统配置
 * 2. 否则使用用户自己的 OSS 配置（如果有的话）
 * 3. 用户文件通过 `user_{userId}/` 前缀完全隔离
 */
class OssStorageClient {
  constructor(user) {
    this.user = user;
    this.s3Client = null;
    this.prefix = `user_${user.id}/`; // 用户隔离前缀
    this.useUnifiedConfig = false; // 标记是否使用统一配置
  }

  /**
   * 获取有效的 OSS 配置（优先使用系统配置）
   * @returns {Object} OSS 配置对象
   * @throws {Error} 如果没有可用的配置
   */
  getEffectiveConfig() {
    // 1. 优先检查系统级统一配置
    const unifiedConfig = SettingsDB.getUnifiedOssConfig();
    if (unifiedConfig) {
      console.log(`[OSS存储] 用户 ${this.user.id} 使用系统级统一 OSS 配置`);
      this.useUnifiedConfig = true;
      return {
        oss_provider: unifiedConfig.provider,
        oss_region: unifiedConfig.region,
        oss_access_key_id: unifiedConfig.access_key_id,
        oss_access_key_secret: unifiedConfig.access_key_secret,
        oss_bucket: unifiedConfig.bucket,
        oss_endpoint: unifiedConfig.endpoint
      };
    }

    // 2. 回退到用户自己的配置
    if (this.user.has_oss_config) {
      console.log(`[OSS存储] 用户 ${this.user.id} 使用个人 OSS 配置`);
      this.useUnifiedConfig = false;
      return {
        oss_provider: this.user.oss_provider,
        oss_region: this.user.oss_region,
        oss_access_key_id: this.user.oss_access_key_id,
        oss_access_key_secret: this.user.oss_access_key_secret,
        oss_bucket: this.user.oss_bucket,
        oss_endpoint: this.user.oss_endpoint
      };
    }

    // 3. 没有可用配置
    throw new Error('OSS 存储未配置，请联系管理员配置系统级 OSS 服务');
  }

  /**
   * 验证 OSS 配置是否完整
   * @throws {Error} 配置不完整时抛出错误
   */
  validateConfig(config) {
    const { oss_provider, oss_access_key_id, oss_access_key_secret, oss_bucket } = config;

    if (!oss_provider || !['aliyun', 'tencent', 'aws'].includes(oss_provider)) {
      throw new Error('无效的 OSS 服务商，必须是 aliyun、tencent 或 aws');
    }
    if (!oss_access_key_id || oss_access_key_id.trim() === '') {
      throw new Error('OSS Access Key ID 不能为空');
    }
    if (!oss_access_key_secret || oss_access_key_secret.trim() === '') {
      throw new Error('OSS Access Key Secret 不能为空');
    }
    if (!oss_bucket || oss_bucket.trim() === '') {
      throw new Error('OSS 存储桶名称不能为空');
    }
  }

  /**
   * 根据服务商构建 S3 配置
   * @param {Object} config - OSS 配置对象
   * @returns {Object} S3 客户端配置
   */
  buildConfig(config) {
    // 先验证配置
    this.validateConfig(config);

    const { oss_provider, oss_region, oss_access_key_id, oss_access_key_secret, oss_endpoint } = config;

    // AWS S3 默认配置
    let s3Config = {
      region: oss_region || 'us-east-1',
      credentials: {
        accessKeyId: oss_access_key_id,
        secretAccessKey: oss_access_key_secret
      },
      // 请求超时配置
      requestHandler: {
        requestTimeout: 30000, // 30秒超时
        httpsAgent: { timeout: 30000 }
      },
      // 重试配置
      maxAttempts: 3,
      // 禁用AWS特定的计算功能，提升阿里云OSS兼容性
      disableNormalizeBucketName: true,
      // 禁用MD5校验和计算（阿里云OSS不完全支持AWS的checksum特性）
      checksumValidation: false
    };

    // 阿里云 OSS
    if (oss_provider === 'aliyun') {
      // 规范化 region：确保格式为 oss-cn-xxx
      let region = oss_region || 'oss-cn-hangzhou';
      if (!region.startsWith('oss-')) {
        region = 'oss-' + region;
      }
      s3Config.region = region;

      if (!oss_endpoint) {
        // 默认 endpoint 格式：https://{region}.aliyuncs.com
        s3Config.endpoint = `https://${region}.aliyuncs.com`;
      } else {
        // 确保 endpoint 以 https:// 或 http:// 开头
        s3Config.endpoint = oss_endpoint.startsWith('http') ? oss_endpoint : `https://${oss_endpoint}`;
      }
      // 阿里云 OSS 使用 virtual-hosted-style，但需要设置 forcePathStyle 为 false
      s3Config.forcePathStyle = false;
      // 阿里云OSS特定配置：禁用AWS特定的计算功能
      s3Config.disableNormalizeBucketName = true;
      s3Config.checksumValidation = false;
    }
    // 腾讯云 COS
    else if (oss_provider === 'tencent') {
      s3Config.region = oss_region || 'ap-guangzhou';
      if (!oss_endpoint) {
        // 默认 endpoint 格式：https://cos.{region}.myqcloud.com
        s3Config.endpoint = `https://cos.${s3Config.region}.myqcloud.com`;
      } else {
        s3Config.endpoint = oss_endpoint.startsWith('http') ? oss_endpoint : `https://${oss_endpoint}`;
      }
      // 腾讯云 COS 使用 virtual-hosted-style
      s3Config.forcePathStyle = false;
    }
    // AWS S3 或其他兼容服务
    else {
      if (oss_endpoint) {
        s3Config.endpoint = oss_endpoint.startsWith('http') ? oss_endpoint : `https://${oss_endpoint}`;
        // 自定义 endpoint（如 MinIO）通常需要 path-style
        s3Config.forcePathStyle = true;
      }
      // AWS 使用默认 endpoint，无需额外配置
    }

    return s3Config;
  }

  /**
   * 连接 OSS 服务（初始化 S3 客户端）
   */
  async connect() {
    try {
      // 获取有效的 OSS 配置（系统配置优先）
      const ossConfig = this.getEffectiveConfig();
      const s3Config = this.buildConfig(ossConfig);

      // 保存当前使用的配置（供其他方法使用）
      this.currentConfig = ossConfig;

      this.s3Client = new S3Client(s3Config);
      console.log(`[OSS存储] 已连接: ${ossConfig.oss_provider}, bucket: ${ossConfig.oss_bucket}, 使用统一配置: ${this.useUnifiedConfig}`);
      return this;
    } catch (error) {
      console.error(`[OSS存储] 连接失败:`, error.message);
      throw new Error(`OSS 连接失败: ${error.message}`);
    }
  }

  /**
   * 获取当前使用的 bucket 名称
   * @returns {string}
   */
  getBucket() {
    if (this.currentConfig && this.currentConfig.oss_bucket) {
      return this.currentConfig.oss_bucket;
    }
    // 回退到用户配置（向后兼容）
    return this.user.oss_bucket;
  }

  /**
   * 获取当前使用的 provider
   * @returns {string}
   */
  getProvider() {
    if (this.currentConfig && this.currentConfig.oss_provider) {
      return this.currentConfig.oss_provider;
    }
    // 回退到用户配置（向后兼容）
    return this.user.oss_provider;
  }

  /**
   * 获取对象的完整 Key（带用户前缀）
   * 增强安全检查，防止路径遍历攻击
   */
  getObjectKey(relativePath) {
    // 0. 输入类型验证
    if (relativePath === null || relativePath === undefined) {
      return this.prefix; // null/undefined 返回根目录
    }

    if (typeof relativePath !== 'string') {
      throw new Error('无效的路径类型');
    }

    // 1. 检查空字节注入（%00, \x00）和其他危险字符
    if (relativePath.includes('\x00') || relativePath.includes('%00')) {
      console.warn('[OSS安全] 检测到空字节注入尝试:', relativePath);
      throw new Error('路径包含非法字符');
    }

    // 2. 先进行 URL 解码（防止双重编码绕过）
    let decoded = relativePath;
    try {
      decoded = decodeURIComponent(relativePath);
    } catch (e) {
      // 解码失败使用原始值
    }

    // 3. 检查解码后的空字节
    if (decoded.includes('\x00')) {
      console.warn('[OSS安全] 检测到编码的空字节注入:', relativePath);
      throw new Error('路径包含非法字符');
    }

    // 4. 规范化路径：统一使用正斜杠（OSS 使用正斜杠作为分隔符）
    let normalized = decoded
      .replace(/\\/g, '/')  // 将反斜杠转换为正斜杠
      .replace(/\/+/g, '/'); // 合并多个连续斜杠

    // 5. 严格检查：路径中不允许包含 ..（防止目录遍历）
    // 检查各种变体：../, /../, /..
    if (normalized.includes('..')) {
      console.warn('[OSS安全] 检测到目录遍历尝试:', relativePath);
      throw new Error('路径包含非法字符');
    }

    // 6. 移除开头的斜杠
    normalized = normalized.replace(/^\/+/, '');

    // 7. 移除结尾的斜杠（除非是根目录）
    if (normalized.length > 0 && normalized !== '/') {
      normalized = normalized.replace(/\/+$/, '');
    }

    // 8. 空路径或 . 表示根目录
    if (normalized === '' || normalized === '.') {
      return this.prefix;
    }

    // 9. 拼接用户前缀（确保不会产生双斜杠）
    const objectKey = this.prefix + normalized;

    // 10. 最终验证：确保生成的 key 以用户前缀开头（双重保险）
    if (!objectKey.startsWith(this.prefix)) {
      console.warn('[OSS安全] Key 前缀验证失败:', { input: relativePath, key: objectKey, prefix: this.prefix });
      throw new Error('非法路径访问');
    }

    return objectKey;
  }

  /**
   * 列出目录内容
   * 支持分页，可列出超过 1000 个文件的目录
   * @param {string} dirPath - 目录路径
   * @param {number} maxItems - 最大返回数量，默认 10000，设为 0 表示不限制
   */
  async list(dirPath, maxItems = 10000) {
    try {
      let prefix = this.getObjectKey(dirPath);
      const bucket = this.getBucket();

      // 确保前缀以斜杠结尾（除非是根目录）
      if (prefix && !prefix.endsWith('/')) {
        prefix = prefix + '/';
      }

      const items = [];
      const dirSet = new Set(); // 用于去重目录
      let continuationToken = undefined;
      const MAX_KEYS_PER_REQUEST = 1000;

      do {
        const command = new ListObjectsV2Command({
          Bucket: bucket,
          Prefix: prefix,
          Delimiter: '/', // 使用分隔符模拟目录结构
          MaxKeys: MAX_KEYS_PER_REQUEST,
          ContinuationToken: continuationToken
        });

        const response = await this.s3Client.send(command);
        continuationToken = response.NextContinuationToken;

        // 处理"子目录"（CommonPrefixes）
        if (response.CommonPrefixes) {
          for (const prefixObj of response.CommonPrefixes) {
            const dirName = prefixObj.Prefix.substring(prefix.length).replace(/\/$/, '');
            if (dirName && !dirSet.has(dirName)) {
              dirSet.add(dirName);
              items.push({
                name: dirName,
                type: 'd',
                size: 0,
                modifyTime: Date.now()
              });
            }
          }
        }

        // 处理文件（Contents）
        if (response.Contents) {
          for (const obj of response.Contents) {
            const key = obj.Key;
            // 跳过目录标记本身（以斜杠结尾的空对象）
            if (key === prefix || key.endsWith('/')) {
              continue;
            }
            const fileName = key.substring(prefix.length);
            // 跳过包含子路径的文件（不应该出现，但以防万一）
            if (fileName && !fileName.includes('/')) {
              items.push({
                name: fileName,
                type: '-',
                size: obj.Size || 0,
                modifyTime: obj.LastModified ? obj.LastModified.getTime() : Date.now()
              });
            }
          }
        }

        // 检查是否达到最大数量限制
        if (maxItems > 0 && items.length >= maxItems) {
          console.log(`[OSS存储] 列出目录达到限制: ${dirPath} (${items.length}/${maxItems})`);
          break;
        }

      } while (continuationToken);

      return items;
    } catch (error) {
      console.error(`[OSS存储] 列出目录失败: ${dirPath}`, error.message);

      // 判断错误类型并给出友好的错误信息
      if (error.name === 'NoSuchBucket') {
        throw new Error('OSS 存储桶不存在，请检查配置');
      } else if (error.name === 'AccessDenied') {
        throw new Error('OSS 访问被拒绝，请检查权限配置');
      } else if (error.name === 'InvalidAccessKeyId') {
        throw new Error('OSS Access Key 无效，请重新配置');
      }
      throw new Error(`列出目录失败: ${error.message}`);
    }
  }

  /**
   * 上传文件（直接上传，简单高效）
   * @param {string} localPath - 本地文件路径
   * @param {string} remotePath - 远程文件路径
   */
  async put(localPath, remotePath) {
    let bucket = '';
    let key = '';
    let uploadId = null;
    let fileHandle = null;
    try {
      key = this.getObjectKey(remotePath);
      bucket = this.getBucket();

      // 检查本地文件是否存在
      if (!fs.existsSync(localPath)) {
        throw new Error(`本地文件不存在: ${localPath}`);
      }

      const fileStats = fs.statSync(localPath);
      const fileSize = fileStats.size;

      if (fileSize <= OSS_SINGLE_UPLOAD_THRESHOLD) {
        // 小文件保持单请求上传，兼容性更高。
        const fileContent = fs.readFileSync(localPath);
        const command = new PutObjectCommand({
          Bucket: bucket,
          Key: key,
          Body: fileContent,
          ContentLength: fileSize,
          ChecksumAlgorithm: undefined
        });
        await this.s3Client.send(command);
        console.log(`[OSS存储] 上传成功: ${key} (${formatFileSize(fileSize)})`);
        return;
      }

      // 大文件自动切换到分片上传，避免整文件读入内存和 5GB 单请求上限。
      const minPartSizeByCount = Math.ceil(fileSize / OSS_MULTIPART_MAX_PARTS);
      const alignedMinPartSize = Math.ceil(
        Math.max(minPartSizeByCount, OSS_MULTIPART_MIN_PART_SIZE) / (1024 * 1024)
      ) * 1024 * 1024;
      const partSize = Math.max(OSS_MULTIPART_DEFAULT_PART_SIZE, alignedMinPartSize);
      const estimatedParts = Math.ceil(fileSize / partSize);
      if (estimatedParts > OSS_MULTIPART_MAX_PARTS) {
        throw new Error(`文件过大，分片数量超限 (${estimatedParts}/${OSS_MULTIPART_MAX_PARTS})`);
      }

      const createResp = await this.s3Client.send(new CreateMultipartUploadCommand({
        Bucket: bucket,
        Key: key
      }));
      uploadId = createResp?.UploadId || null;
      if (!uploadId) {
        throw new Error('创建分片上传会话失败：缺少 UploadId');
      }

      const uploadedParts = [];
      let offset = 0;
      let partNumber = 1;
      fileHandle = await fs.promises.open(localPath, 'r');

      while (offset < fileSize) {
        const remaining = fileSize - offset;
        const currentPartSize = Math.min(partSize, remaining);
        const buffer = Buffer.allocUnsafe(currentPartSize);
        const { bytesRead } = await fileHandle.read(buffer, 0, currentPartSize, offset);
        if (bytesRead <= 0) {
          throw new Error('读取本地文件失败，分片上传中断');
        }

        const body = bytesRead === currentPartSize ? buffer : buffer.subarray(0, bytesRead);
        const partResp = await this.s3Client.send(new UploadPartCommand({
          Bucket: bucket,
          Key: key,
          UploadId: uploadId,
          PartNumber: partNumber,
          Body: body,
          ContentLength: bytesRead
        }));

        if (!partResp?.ETag) {
          throw new Error(`分片 ${partNumber} 上传失败：服务端未返回 ETag`);
        }
        uploadedParts.push({
          ETag: partResp.ETag,
          PartNumber: partNumber
        });

        offset += bytesRead;
        if (partNumber === 1 || partNumber % 10 === 0 || offset >= fileSize) {
          console.log(
            `[OSS存储] 上传分片进度: ${key} ${partNumber}/${estimatedParts} (${formatFileSize(offset)}/${formatFileSize(fileSize)})`
          );
        }
        partNumber += 1;
      }

      await this.s3Client.send(new CompleteMultipartUploadCommand({
        Bucket: bucket,
        Key: key,
        UploadId: uploadId,
        MultipartUpload: { Parts: uploadedParts }
      }));
      uploadId = null;
      console.log(
        `[OSS存储] 分片上传完成: ${key} (${formatFileSize(fileSize)}, ${uploadedParts.length} 片)`
      );

    } catch (error) {
      if (uploadId && bucket && key) {
        try {
          await this.s3Client.send(new AbortMultipartUploadCommand({
            Bucket: bucket,
            Key: key,
            UploadId: uploadId
          }));
          console.warn(`[OSS存储] 已中止失败的分片上传: ${key}`);
        } catch (abortError) {
          console.error(`[OSS存储] 中止分片上传失败: ${key}`, abortError.message);
        }
      }

      console.error(`[OSS存储] 上传失败: ${remotePath}`, error.message);

      // 判断错误类型并给出友好的错误信息
      if (error.name === 'NoSuchBucket') {
        throw new Error('OSS 存储桶不存在，请检查配置');
      } else if (error.name === 'AccessDenied') {
        throw new Error('OSS 访问被拒绝，请检查权限配置');
      } else if (error.name === 'EntityTooLarge') {
        throw new Error('文件过大，超过了 OSS 允许的最大大小');
      } else if (error.code === 'ENOENT') {
        throw new Error(`本地文件不存在: ${localPath}`);
      }
      throw new Error(`文件上传失败: ${error.message}`);
    } finally {
      if (fileHandle) {
        await fileHandle.close().catch(() => {});
      }
    }
  }

  /**
   * 删除文件或文件夹
   * ===== P0 性能优化：返回删除的文件大小，用于更新存储使用量缓存 =====
   * @returns {Promise<{size: number}>} 返回删除的文件总大小（字节）
   */
  async delete(filePath) {
    try {
      const key = this.getObjectKey(filePath);
      const bucket = this.getBucket();

      // 检查是文件还是目录（忽略不存在的文件）
      let statResult;
      try {
        statResult = await this.stat(filePath);
      } catch (statError) {
        if (statError.message && statResult?.message.includes('不存在')) {
          console.warn(`[OSS存储] 文件不存在，跳过删除: ${key}`);
          return { size: 0 }; // 文件不存在，返回大小为 0
        }
        throw statError; // 其他错误继续抛出
      }

      let totalDeletedSize = 0;

      if (statResult.isDirectory) {
        // 删除目录：列出所有对象并批量删除
        // 使用分页循环处理超过 1000 个对象的情况
        let continuationToken = null;
        let totalDeletedCount = 0;
        const MAX_DELETE_BATCH = 1000; // AWS S3 单次最多删除 1000 个对象

        do {
          const listCommand = new ListObjectsV2Command({
            Bucket: bucket,
            Prefix: key,
            MaxKeys: MAX_DELETE_BATCH,
            ContinuationToken: continuationToken
          });

          const listResponse = await this.s3Client.send(listCommand);
          continuationToken = listResponse.NextContinuationToken;

          if (listResponse.Contents && listResponse.Contents.length > 0) {
            // 累加删除的文件大小
            for (const obj of listResponse.Contents) {
              totalDeletedSize += obj.Size || 0;
            }

            // 批量删除当前批次的对象
            const deleteCommand = new DeleteObjectsCommand({
              Bucket: bucket,
              Delete: {
                Objects: listResponse.Contents.map(obj => ({ Key: obj.Key })),
                Quiet: false
              }
            });

            const deleteResult = await this.s3Client.send(deleteCommand);

            // 检查删除结果
            if (deleteResult.Errors && deleteResult.Errors.length > 0) {
              console.warn(`[OSS存储] 部分对象删除失败:`, deleteResult.Errors);
            }

            totalDeletedCount += listResponse.Contents.length;
          }
        } while (continuationToken);

        if (totalDeletedCount > 0) {
          console.log(`[OSS存储] 删除目录: ${key} (${totalDeletedCount} 个对象, ${totalDeletedSize} 字节)`);
        }

        return { size: totalDeletedSize };
      } else {
        // 删除单个文件（使用DeleteObjectCommand）
        // 获取文件大小
        const size = statResult.size || 0;
        totalDeletedSize = size;

        const { DeleteObjectCommand } = require('@aws-sdk/client-s3');
        const command = new DeleteObjectCommand({
          Bucket: bucket,
          Key: key
        });

        await this.s3Client.send(command);
        console.log(`[OSS存储] 删除文件: ${key} (${size} 字节)`);

        return { size: totalDeletedSize };
      }
    } catch (error) {
      console.error(`[OSS存储] 删除失败: ${filePath}`, error.message);

      // 判断错误类型并给出友好的错误信息
      if (error.name === 'NoSuchBucket') {
        throw new Error('OSS 存储桶不存在，请检查配置');
      } else if (error.name === 'AccessDenied') {
        throw new Error('OSS 访问被拒绝，请检查权限配置');
      }
      throw new Error(`删除文件失败: ${error.message}`);
    }
  }

  /**
   * 重命名文件或目录（OSS 不支持直接重命名，需要复制后删除）
   * 支持文件和目录的重命名
   * @param {string} oldPath - 原路径
   * @param {string} newPath - 新路径
   */
  async rename(oldPath, newPath) {
    const oldKey = this.getObjectKey(oldPath);
    const newKey = this.getObjectKey(newPath);
    const bucket = this.getBucket(); // 使用getBucket()方法以支持系统级统一OSS配置

    // 验证源和目标不同
    if (oldKey === newKey) {
      console.log(`[OSS存储] 源路径和目标路径相同，跳过: ${oldKey}`);
      return;
    }

    let copySuccess = false;

    try {
      // 检查源文件是否存在
      const statResult = await this.stat(oldPath);

      // 如果是目录，执行目录重命名
      if (statResult.isDirectory) {
        await this._renameDirectory(oldPath, newPath);
        return;
      }

      // 使用 CopyObjectCommand 复制文件
      // CopySource 格式：bucket/key，需要对 key 中的特殊字符进行编码
      // 但保留路径分隔符（/）不编码
      const encodedOldKey = oldKey.split('/').map(segment => encodeURIComponent(segment)).join('/');
      const copySource = `${bucket}/${encodedOldKey}`;

      const copyCommand = new CopyObjectCommand({
        Bucket: bucket,
        CopySource: copySource,
        Key: newKey
      });

      await this.s3Client.send(copyCommand);
      copySuccess = true;

      // 复制成功后删除原文件（使用DeleteObjectCommand删除单个文件）
      const { DeleteObjectCommand } = require('@aws-sdk/client-s3');
      const deleteCommand = new DeleteObjectCommand({
        Bucket: bucket,
        Key: oldKey
      });
      await this.s3Client.send(deleteCommand);

      console.log(`[OSS存储] 重命名: ${oldKey} -> ${newKey}`);
    } catch (error) {
      console.error(`[OSS存储] 重命名失败: ${oldPath} -> ${newPath}`, error.message);

      // 如果复制成功但删除失败，尝试回滚（删除新复制的文件）
      if (copySuccess) {
        try {
          console.log(`[OSS存储] 尝试回滚：删除已复制的文件 ${newKey}`);
          const { DeleteObjectCommand } = require('@aws-sdk/client-s3');
          const deleteCommand = new DeleteObjectCommand({
            Bucket: bucket,
            Key: newKey
          });
          await this.s3Client.send(deleteCommand);
          console.log(`[OSS存储] 回滚成功：已删除 ${newKey}`);
        } catch (rollbackError) {
          console.error(`[OSS存储] 回滚失败: ${rollbackError.message}`);
        }
      }

      // 判断错误类型并给出友好的错误信息
      if (error.name === 'NoSuchBucket') {
        throw new Error('OSS 存储桶不存在，请检查配置');
      } else if (error.name === 'AccessDenied') {
        throw new Error('OSS 访问被拒绝，请检查权限配置');
      } else if (error.name === 'NoSuchKey') {
        throw new Error('源文件不存在');
      }
      throw new Error(`重命名文件失败: ${error.message}`);
    }
  }

  /**
   * 重命名目录（内部方法）
   * 通过遍历目录下所有对象，逐个复制到新位置后删除原对象
   * 使用事务标记机制防止竞态条件
   * @param {string} oldPath - 原目录路径
   * @param {string} newPath - 新目录路径
   * @private
   */
  async _renameDirectory(oldPath, newPath) {
    const oldPrefix = this.getObjectKey(oldPath);
    const newPrefix = this.getObjectKey(newPath);
    const bucket = this.getBucket();

    // 确保前缀以斜杠结尾
    const oldPrefixWithSlash = oldPrefix.endsWith('/') ? oldPrefix : `${oldPrefix}/`;
    const newPrefixWithSlash = newPrefix.endsWith('/') ? newPrefix : `${newPrefix}/`;

    // 生成事务标记文件
    const timestamp = Date.now();
    const markerKey = `${this.prefix}.rename_pending_${timestamp}.json`;

    // 标记文件内容（用于恢复）
    const markerContent = JSON.stringify({
      oldPrefix: oldPrefixWithSlash,
      newPrefix: newPrefixWithSlash,
      timestamp: timestamp,
      phase: 'copying' // 标记当前阶段：copying（复制中）、deleting（删除中）
    });

    let continuationToken = null;
    let copiedKeys = [];
    let totalCount = 0;

    try {
      // 步骤1：创建事务标记文件（标识重命名操作开始）
      console.log(`[OSS存储] 创建重命名事务标记: ${markerKey}`);
      const putMarkerCommand = new PutObjectCommand({
        Bucket: bucket,
        Key: markerKey,
        Body: markerContent,
        ContentType: 'application/json'
      });
      await this.s3Client.send(putMarkerCommand);

      // 步骤2：复制所有对象到新位置
      do {
        const listCommand = new ListObjectsV2Command({
          Bucket: bucket,
          Prefix: oldPrefixWithSlash,
          ContinuationToken: continuationToken,
          MaxKeys: 1000
        });

        const listResponse = await this.s3Client.send(listCommand);
        continuationToken = listResponse.NextContinuationToken;

        if (listResponse.Contents && listResponse.Contents.length > 0) {
          for (const obj of listResponse.Contents) {
            // 计算新的 key（替换前缀）
            const newKey = newPrefixWithSlash + obj.Key.substring(oldPrefixWithSlash.length);

            // 复制对象
            const encodedOldKey = obj.Key.split('/').map(segment => encodeURIComponent(segment)).join('/');
            const copyCommand = new CopyObjectCommand({
              Bucket: bucket,
              CopySource: `${bucket}/${encodedOldKey}`,
              Key: newKey
            });

            await this.s3Client.send(copyCommand);
            copiedKeys.push({ oldKey: obj.Key, newKey });
            totalCount++;
          }
        }
      } while (continuationToken);

      // 步骤3：更新标记文件状态为 deleting（复制完成，开始删除）
      const updatedMarkerContent = JSON.stringify({
        oldPrefix: oldPrefixWithSlash,
        newPrefix: newPrefixWithSlash,
        timestamp: timestamp,
        phase: 'deleting'
      });
      await this.s3Client.send(new PutObjectCommand({
        Bucket: bucket,
        Key: markerKey,
        Body: updatedMarkerContent,
        ContentType: 'application/json'
      }));

      // 步骤4：删除所有原对象（批量删除）
      if (copiedKeys.length > 0) {
        for (let i = 0; i < copiedKeys.length; i += 1000) {
          const batch = copiedKeys.slice(i, i + 1000);
          const deleteCommand = new DeleteObjectsCommand({
            Bucket: bucket,
            Delete: {
              Objects: batch.map(item => ({ Key: item.oldKey })),
              Quiet: true
            }
          });
          await this.s3Client.send(deleteCommand);
        }
      }

      // 步骤5：删除事务标记文件（操作完成）
      await this.s3Client.send(new DeleteObjectsCommand({
        Bucket: bucket,
        Delete: {
          Objects: [{ Key: markerKey }],
          Quiet: true
        }
      }));

      console.log(`[OSS存储] 重命名目录完成: ${oldPath} -> ${newPath} (${totalCount} 个对象)`);

    } catch (error) {
      // 如果出错，尝试回滚
      console.error(`[OSS存储] 目录重命名失败: ${error.message}`);

      if (copiedKeys.length > 0) {
        console.warn(`[OSS存储] 尝试回滚已复制的 ${copiedKeys.length} 个对象...`);
        try {
          // 回滚：删除已复制的新对象
          for (let i = 0; i < copiedKeys.length; i += 1000) {
            const batch = copiedKeys.slice(i, i + 1000);
            const deleteCommand = new DeleteObjectsCommand({
              Bucket: bucket,
              Delete: {
                Objects: batch.map(item => ({ Key: item.newKey })),
                Quiet: true
              }
            });
            await this.s3Client.send(deleteCommand);
          }
          console.log(`[OSS存储] 回滚成功`);
        } catch (rollbackError) {
          console.error(`[OSS存储] 回滚失败: ${rollbackError.message}`);
        }
      }

      // 清理事务标记文件（如果还存在）
      try {
        await this.s3Client.send(new DeleteObjectsCommand({
          Bucket: bucket,
          Delete: {
            Objects: [{ Key: markerKey }],
            Quiet: true
          }
        }));
      } catch (markerError) {
        // 忽略标记文件删除错误
      }

      throw new Error(`重命名目录失败: ${error.message}`);
    }
  }

  /**
   * 获取文件信息
   */
  async stat(filePath) {
    const key = this.getObjectKey(filePath);
    const bucket = this.getBucket(); // 使用getBucket()方法以支持系统级统一OSS配置

    try {
      const command = new HeadObjectCommand({
        Bucket: bucket,
        Key: key
      });

      const response = await this.s3Client.send(command);
      return {
        size: response.ContentLength || 0,
        modifyTime: response.LastModified ? response.LastModified.getTime() : Date.now(),
        isDirectory: false
      };
    } catch (error) {
      if (error.name === 'NotFound' || error.$metadata?.httpStatusCode === 404) {
        // 可能是目录，尝试列出前缀
        const listCommand = new ListObjectsV2Command({
          Bucket: bucket,
          Prefix: key.endsWith('/') ? key : key + '/',
          MaxKeys: 1
        });

        try {
          const listResponse = await this.s3Client.send(listCommand);
          if (listResponse.Contents && listResponse.Contents.length > 0) {
            return { isDirectory: true, size: 0, modifyTime: Date.now() };
          }
        } catch (listError) {
          // 忽略列表错误
        }
      }
      throw new Error(`对象不存在: ${key}`);
    }
  }

  /**
   * 创建文件读取流（异步方法）
   * @returns {Promise<Readable>} 返回可读流 Promise
   */
  async createReadStream(filePath) {
    const key = this.getObjectKey(filePath);
    const bucket = this.getBucket();

    const command = new GetObjectCommand({
      Bucket: bucket,
      Key: key
    });

    try {
      const response = await this.s3Client.send(command);
      // AWS SDK v3 返回的 Body 是一个 IncomingMessage 类型的流
      return response.Body;
    } catch (error) {
      console.error(`[OSS存储] 创建读取流失败: ${key}`, error.message);
      throw error;
    }
  }

  /**
   * 创建文件夹（通过创建空对象模拟）
   * OSS 中文件夹实际上是一个以斜杠结尾的空对象
   */
  async mkdir(dirPath) {
    try {
      const key = this.getObjectKey(dirPath);
      const bucket = this.getBucket();

      // OSS 中文件夹通过以斜杠结尾的空对象模拟
      const folderKey = key.endsWith('/') ? key : `${key}/`;

      const command = new PutObjectCommand({
        Bucket: bucket,
        Key: folderKey,
        Body: '', // 空内容
        ContentType: 'application/x-directory'
      });

      await this.s3Client.send(command);
      console.log(`[OSS存储] 创建文件夹: ${folderKey}`);
    } catch (error) {
      console.error(`[OSS存储] 创建文件夹失败: ${dirPath}`, error.message);
      if (error.name === 'AccessDenied') {
        throw new Error('OSS 访问被拒绝，请检查权限配置');
      }
      throw new Error(`创建文件夹失败: ${error.message}`);
    }
  }

  /**
   * 获取签名下载 URL（用于分享链接，支持私有 bucket）
   * @param {string} filePath - 文件路径
   * @param {number} expiresIn - 过期时间（秒），默认 3600 秒（1小时）
   * @returns {Promise<string>} 签名 URL
   */
  async getPresignedUrl(filePath, expiresIn = 3600) {
    const key = this.getObjectKey(filePath);
    const bucket = this.getBucket();

    try {
      const command = new GetObjectCommand({
        Bucket: bucket,
        Key: key
      });

      // 使用 AWS SDK 的 getSignedUrl 生成真正的签名 URL
      const signedUrl = await getSignedUrl(this.s3Client, command, {
        expiresIn: Math.min(expiresIn, 604800) // 最大 7 天
      });

      return signedUrl;
    } catch (error) {
      console.error(`[OSS存储] 生成签名 URL 失败: ${filePath}`, error.message);
      throw new Error(`生成签名 URL 失败: ${error.message}`);
    }
  }

  /**
   * 获取公开 URL（仅适用于公共读的 bucket）
   * @deprecated 建议使用 getPresignedUrl 代替
   */
  getPublicUrl(filePath) {
    const key = this.getObjectKey(filePath);
    const bucket = this.getBucket();
    const provider = this.getProvider();
    const region = this.s3Client.config.region;

    let baseUrl;
    if (provider === 'aliyun') {
      // 阿里云 OSS 公开 URL 格式
      const ossRegion = region.startsWith('oss-') ? region : `oss-${region}`;
      baseUrl = `https://${bucket}.${ossRegion}.aliyuncs.com`;
    } else if (provider === 'tencent') {
      // 腾讯云 COS 公开 URL 格式
      baseUrl = `https://${bucket}.cos.${region}.myqcloud.com`;
    } else {
      // AWS S3 公开 URL 格式
      baseUrl = `https://${bucket}.s3.${region}.amazonaws.com`;
    }

    // 对 key 中的特殊字符进行 URL 编码，但保留路径分隔符
    const encodedKey = key.split('/').map(segment => encodeURIComponent(segment)).join('/');
    return `${baseUrl}/${encodedKey}`;
  }

  /**
   * 获取上传签名 URL（用于前端直传）
   * @param {string} filePath - 文件路径
   * @param {number} expiresIn - 过期时间（秒），默认 900 秒（15分钟）
   * @param {string} contentType - 文件 MIME 类型
   * @returns {Promise<string>} 签名 URL
   */
  async getUploadPresignedUrl(filePath, expiresIn = 900, contentType = 'application/octet-stream') {
    const key = this.getObjectKey(filePath);
    const bucket = this.getBucket();

    try {
      const command = new PutObjectCommand({
        Bucket: bucket,
        Key: key,
        ContentType: contentType
      });

      const signedUrl = await getSignedUrl(this.s3Client, command, {
        expiresIn: Math.min(expiresIn, 3600) // 上传 URL 最大 1 小时
      });

      return signedUrl;
    } catch (error) {
      console.error(`[OSS存储] 生成上传签名 URL 失败: ${filePath}`, error.message);
      throw new Error(`生成上传签名 URL 失败: ${error.message}`);
    }
  }

  /**
   * 检查文件或目录是否存在
   * @param {string} filePath - 文件路径
   * @returns {Promise<boolean>}
   */
  async exists(filePath) {
    try {
      await this.stat(filePath);
      return true;
    } catch (error) {
      return false;
    }
  }

  /**
   * 格式化文件大小
   */
  formatSize(bytes) {
    return formatFileSize(bytes);
  }

  /**
   * 关闭连接（S3Client 无需显式关闭）
   */
  async end() {
    this.s3Client = null;
  }
}

/**
 * 将流转换为 Buffer（辅助函数）
 */
async function streamToBuffer(stream) {
  return new Promise((resolve, reject) => {
    const chunks = [];
    stream.on('data', (chunk) => chunks.push(chunk));
    stream.on('error', reject);
    stream.on('end', () => resolve(Buffer.concat(chunks)));
  });
}


module.exports = {
  StorageInterface,
  LocalStorageClient,
  OssStorageClient,
  formatFileSize,     // 导出共享的工具函数
  formatOssError      // 导出 OSS 错误格式化函数
};
