// 加载环境变量（确保在 server.js 之前也能读取）
require('dotenv').config();

const Database = require('better-sqlite3');
const bcrypt = require('bcryptjs');
const path = require('path');
const fs = require('fs');
const crypto = require('crypto');

// 引入加密工具（用于敏感数据加密存储）
const { encryptSecret, decryptSecret, validateEncryption } = require('./utils/encryption');

// 验证加密系统在启动时正常工作
try {
  validateEncryption();
} catch (error) {
  console.error('[安全] 加密系统验证失败，服务无法启动');
  console.error('[安全] 请检查 ENCRYPTION_KEY 配置');
  process.exit(1);
}

// 数据库路径配置
// 优先使用环境变量 DATABASE_PATH，默认为 ./data/database.db
const DEFAULT_DB_PATH = path.join(__dirname, 'data', 'database.db');
const dbPath = process.env.DATABASE_PATH
  ? path.resolve(__dirname, process.env.DATABASE_PATH)
  : DEFAULT_DB_PATH;

// 确保数据库目录存在
const dbDir = path.dirname(dbPath);
if (!fs.existsSync(dbDir)) {
  fs.mkdirSync(dbDir, { recursive: true });
  console.log(`[数据库] 创建目录: ${dbDir}`);
}

console.log(`[数据库] 路径: ${dbPath}`);

// 创建或连接数据库
const db = new Database(dbPath);
const DEFAULT_LOCAL_STORAGE_QUOTA_BYTES = 1024 * 1024 * 1024; // 1GB
const DEFAULT_OSS_STORAGE_QUOTA_BYTES = 1024 * 1024 * 1024;   // 1GB

// ===== 性能优化配置（P0 优先级修复） =====

// 1. 启用 WAL 模式（Write-Ahead Logging）
// 优势：支持并发读写，大幅提升数据库性能
db.pragma('journal_mode = WAL');

// 2. 配置同步模式为 NORMAL
// 性能提升：在安全性和性能之间取得平衡，比 FULL 模式快很多
db.pragma('synchronous = NORMAL');

// 3. 增加缓存大小到 64MB
// 性能提升：减少磁盘 I/O，缓存更多数据页和索引页
// 负值表示 KB，-64000 = 64MB
db.pragma('cache_size = -64000');

// 4. 临时表存储在内存中
// 性能提升：避免临时表写入磁盘，加速排序和分组操作
db.pragma('temp_store = MEMORY');

// 5. 启用外键约束
db.pragma('foreign_keys = ON');

console.log('[数据库性能优化] ✓ WAL 模式已启用');
console.log('[数据库性能优化] ✓ 同步模式: NORMAL');
console.log('[数据库性能优化] ✓ 缓存大小: 64MB');
console.log('[数据库性能优化] ✓ 临时表存储: 内存');

// ===== 第二轮修复：WAL 文件定期清理机制 =====

/**
 * 执行数据库检查点（Checkpoint）
 * 将 WAL 文件中的内容写入主数据库文件，并清理 WAL
 * @param {Database} database - 数据库实例
 * @returns {boolean} 是否成功执行
 */
function performCheckpoint(database = db) {
  try {
    // 执行 checkpoint（将 WAL 内容合并到主数据库）
    database.pragma('wal_checkpoint(PASSIVE)');

    // 获取 WAL 文件大小信息
    const walInfo = database.pragma('wal_checkpoint(TRUNCATE)', { simple: true });

    console.log('[WAL清理] ✓ 检查点完成');
    return true;
  } catch (error) {
    console.error('[WAL清理] ✗ 检查点失败:', error.message);
    return false;
  }
}

/**
 * 获取 WAL 文件大小
 * @param {Database} database - 数据库实例
 * @returns {number} WAL 文件大小（字节）
 */
function getWalFileSize(database = db) {
  try {
    const dbPath = database.name;
    const walPath = `${dbPath}-wal`;

    if (fs.existsSync(walPath)) {
      const stats = fs.statSync(walPath);
      return stats.size;
    }

    return 0;
  } catch (error) {
    console.error('[WAL清理] 获取 WAL 文件大小失败:', error.message);
    return 0;
  }
}

/**
 * 启动时检查 WAL 文件大小，如果超过阈值则执行清理
 * @param {number} threshold - 阈值（字节），默认 100MB
 */
function checkWalOnStartup(threshold = 100 * 1024 * 1024) {
  try {
    const walSize = getWalFileSize();

    if (walSize > threshold) {
      console.warn(`[WAL清理] ⚠ 启动时检测到 WAL 文件过大: ${(walSize / 1024 / 1024).toFixed(2)}MB`);
      console.log('[WAL清理] 正在执行自动清理...');

      const success = performCheckpoint();

      if (success) {
        const newSize = getWalFileSize();
        console.log(`[WAL清理] ✓ 清理完成: ${walSize} → ${newSize} 字节`);
      }
    } else {
      console.log(`[WAL清理] ✓ WAL 文件大小正常: ${(walSize / 1024 / 1024).toFixed(2)}MB`);
    }
  } catch (error) {
    console.error('[WAL清理] 启动检查失败:', error.message);
  }
}

/**
 * 设置定期 WAL 检查点
 * 每隔指定时间自动执行一次检查点，防止 WAL 文件无限增长
 * @param {number} intervalHours - 间隔时间（小时），默认 24 小时
 * @returns {NodeJS.Timeout} 定时器 ID，可用于取消
 */
function schedulePeriodicCheckpoint(intervalHours = 24) {
  const intervalMs = intervalHours * 60 * 60 * 1000;

  const timerId = setInterval(() => {
    const walSize = getWalFileSize();

    console.log(`[WAL清理] 定期检查点执行中... (当前 WAL: ${(walSize / 1024 / 1024).toFixed(2)}MB)`);

    performCheckpoint();
  }, intervalMs);

  console.log(`[WAL清理] ✓ 定期检查点已启用: 每 ${intervalHours} 小时执行一次`);

  return timerId;
}

// 立即执行启动时检查
checkWalOnStartup(100 * 1024 * 1024); // 100MB 阈值

// 启动定期检查点（24 小时）
let walCheckpointTimer = null;
if (process.env.WAL_CHECKPOINT_ENABLED !== 'false') {
  const interval = parseInt(process.env.WAL_CHECKPOINT_INTERVAL_HOURS || '24', 10);
  walCheckpointTimer = schedulePeriodicCheckpoint(interval);
} else {
  console.log('[WAL清理] 定期检查点已禁用（WAL_CHECKPOINT_ENABLED=false）');
}

// 导出 WAL 管理函数
const WalManager = {
  performCheckpoint,
  getWalFileSize,
  checkWalOnStartup,
  schedulePeriodicCheckpoint
};

// 初始化数据库表
function initDatabase() {
  // 用户表
  db.exec(`
    CREATE TABLE IF NOT EXISTS users (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      username TEXT UNIQUE NOT NULL,
      email TEXT UNIQUE NOT NULL,
      password TEXT NOT NULL,

      -- OSS配置（可选）
      oss_provider TEXT,
      oss_region TEXT,
      oss_access_key_id TEXT,
      oss_access_key_secret TEXT,
      oss_bucket TEXT,
      oss_endpoint TEXT,

      -- 上传工具API密钥
      upload_api_key TEXT,

      -- 用户状态
      is_admin INTEGER DEFAULT 0,
      is_active INTEGER DEFAULT 1,
      is_banned INTEGER DEFAULT 0,
      has_oss_config INTEGER DEFAULT 0,

      -- 时间戳
      created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
      updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
    )
  `);

  // 分享链接表
  db.exec(`
    CREATE TABLE IF NOT EXISTS shares (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      user_id INTEGER NOT NULL,
      share_code TEXT UNIQUE NOT NULL,
      share_path TEXT NOT NULL,
      share_type TEXT DEFAULT 'file',
      share_password TEXT,
      max_downloads INTEGER DEFAULT NULL,
      ip_whitelist TEXT,
      device_limit TEXT DEFAULT 'all',
      access_time_start TEXT,
      access_time_end TEXT,

      -- 分享统计
      view_count INTEGER DEFAULT 0,
      download_count INTEGER DEFAULT 0,

      -- 时间戳
      created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
      expires_at DATETIME,

      FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
    )
  `);

  // 分享直链表（与 shares 独立，互不影响）
  db.exec(`
    CREATE TABLE IF NOT EXISTS direct_links (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      user_id INTEGER NOT NULL,
      link_code TEXT UNIQUE NOT NULL,
      file_path TEXT NOT NULL,
      file_name TEXT,
      storage_type TEXT DEFAULT 'oss',

      -- 直链统计
      download_count INTEGER DEFAULT 0,
      last_accessed_at DATETIME,

      -- 时间戳
      created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
      expires_at DATETIME,

      FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
    )
  `);

  // 系统设置表
  db.exec(`
    CREATE TABLE IF NOT EXISTS system_settings (
      key TEXT PRIMARY KEY,
      value TEXT NOT NULL,
      updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
    )
  `);

  // 创建索引
  db.exec(`
    -- 基础索引
    CREATE INDEX IF NOT EXISTS idx_users_username ON users(username);
    CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
    CREATE INDEX IF NOT EXISTS idx_users_upload_api_key ON users(upload_api_key);
    CREATE INDEX IF NOT EXISTS idx_shares_code ON shares(share_code);
    CREATE INDEX IF NOT EXISTS idx_shares_user ON shares(user_id);
    CREATE INDEX IF NOT EXISTS idx_shares_expires ON shares(expires_at);
    CREATE INDEX IF NOT EXISTS idx_direct_links_code ON direct_links(link_code);
    CREATE INDEX IF NOT EXISTS idx_direct_links_user ON direct_links(user_id);
    CREATE INDEX IF NOT EXISTS idx_direct_links_expires ON direct_links(expires_at);

    -- ===== 性能优化：复合索引（P0 优先级修复） =====

    -- 1. 分享链接复合索引：share_code + expires_at
    -- 优势：加速分享码查询（最常见的操作），同时过滤过期链接
    -- 使用场景：ShareDB.findByCode, 分享访问验证
    CREATE INDEX IF NOT EXISTS idx_shares_code_expires ON shares(share_code, expires_at);

    -- 直链复合索引：link_code + expires_at
    -- 使用场景：DirectLinkDB.findByCode
    CREATE INDEX IF NOT EXISTS idx_direct_links_code_expires ON direct_links(link_code, expires_at);

    -- 注意：system_logs 表的复合索引在表创建后创建（第372行之后）
    -- 2. 活动日志复合索引：user_id + created_at
    -- 优势：快速查询用户最近的活动记录，支持时间范围过滤
    -- 使用场景：用户活动历史、审计日志查询
    -- CREATE INDEX IF NOT EXISTS idx_logs_user_created ON system_logs(user_id, created_at);

    -- 3. 文件复合索引：user_id + parent_path
    -- 注意：当前系统使用 OSS，不直接存储文件元数据到数据库
    -- 如果未来需要文件系统功能，此索引将优化目录浏览性能
    -- CREATE INDEX IF NOT EXISTS idx_files_user_parent ON files(user_id, parent_path);
  `);

  console.log('[数据库性能优化] ✓ 基础索引已创建');
  console.log('  - idx_shares_code_expires: 分享码+过期时间');
  console.log('  - idx_direct_links_code_expires: 直链码+过期时间');

  // 数据库迁移：添加upload_api_key字段（如果不存在）
  try {
    const columns = db.prepare("PRAGMA table_info(users)").all();
    const hasUploadApiKey = columns.some(col => col.name === 'upload_api_key');

    if (!hasUploadApiKey) {
      db.exec(`ALTER TABLE users ADD COLUMN upload_api_key TEXT`);
      console.log('数据库迁移：添加upload_api_key字段完成');
    }
  } catch (error) {
    console.error('数据库迁移失败:', error);
  }

  // 数据库迁移：添加share_type字段（如果不存在）
  try {
    const shareColumns = db.prepare("PRAGMA table_info(shares)").all();
    const hasShareType = shareColumns.some(col => col.name === 'share_type');

    if (!hasShareType) {
      db.exec(`ALTER TABLE shares ADD COLUMN share_type TEXT DEFAULT 'file'`);
      console.log('数据库迁移：添加share_type字段完成');
    }
  } catch (error) {
    console.error('数据库迁移（share_type）失败:', error);
  }

  // 数据库迁移：分享高级安全策略字段
  try {
    const shareColumns = db.prepare("PRAGMA table_info(shares)").all();
    const hasMaxDownloads = shareColumns.some(col => col.name === 'max_downloads');
    const hasIpWhitelist = shareColumns.some(col => col.name === 'ip_whitelist');
    const hasDeviceLimit = shareColumns.some(col => col.name === 'device_limit');
    const hasAccessTimeStart = shareColumns.some(col => col.name === 'access_time_start');
    const hasAccessTimeEnd = shareColumns.some(col => col.name === 'access_time_end');

    if (!hasMaxDownloads) {
      db.exec('ALTER TABLE shares ADD COLUMN max_downloads INTEGER DEFAULT NULL');
      console.log('数据库迁移：添加 shares.max_downloads 字段完成');
    }
    if (!hasIpWhitelist) {
      db.exec('ALTER TABLE shares ADD COLUMN ip_whitelist TEXT');
      console.log('数据库迁移：添加 shares.ip_whitelist 字段完成');
    }
    if (!hasDeviceLimit) {
      db.exec('ALTER TABLE shares ADD COLUMN device_limit TEXT DEFAULT "all"');
      console.log('数据库迁移：添加 shares.device_limit 字段完成');
    }
    if (!hasAccessTimeStart) {
      db.exec('ALTER TABLE shares ADD COLUMN access_time_start TEXT');
      console.log('数据库迁移：添加 shares.access_time_start 字段完成');
    }
    if (!hasAccessTimeEnd) {
      db.exec('ALTER TABLE shares ADD COLUMN access_time_end TEXT');
      console.log('数据库迁移：添加 shares.access_time_end 字段完成');
    }
  } catch (error) {
    console.error('数据库迁移（分享安全策略）失败:', error);
  }

  // 数据库迁移：邮箱验证字段
  try {
    const columns = db.prepare("PRAGMA table_info(users)").all();
    const hasVerified = columns.some(col => col.name === 'is_verified');
    const hasVerifyToken = columns.some(col => col.name === 'verification_token');
    const hasVerifyExpires = columns.some(col => col.name === 'verification_expires_at');

    if (!hasVerified) {
      db.exec(`ALTER TABLE users ADD COLUMN is_verified INTEGER DEFAULT 0`);
    }
    if (!hasVerifyToken) {
      db.exec(`ALTER TABLE users ADD COLUMN verification_token TEXT`);
    }
    if (!hasVerifyExpires) {
      db.exec(`ALTER TABLE users ADD COLUMN verification_expires_at DATETIME`);
    }

    // 注意：不再自动将未验证用户设为已验证
    // 仅修复 is_verified 为 NULL 的旧数据（添加字段前创建的用户）
    // 这些用户没有 verification_token，说明是在邮箱验证功能上线前注册的
    db.exec(`UPDATE users SET is_verified = 1 WHERE is_verified IS NULL AND verification_token IS NULL`);
  } catch (error) {
    console.error('数据库迁移（邮箱验证）失败:', error);
  }

  // 数据库迁移：密码重置Token表
  try {
    db.exec(`
      CREATE TABLE IF NOT EXISTS password_reset_tokens (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        user_id INTEGER NOT NULL,
        token TEXT UNIQUE NOT NULL,
        expires_at DATETIME NOT NULL,
        used INTEGER DEFAULT 0,
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
      )
    `);
    db.exec(`CREATE INDEX IF NOT EXISTS idx_reset_tokens_token ON password_reset_tokens(token);`);
    db.exec(`CREATE INDEX IF NOT EXISTS idx_reset_tokens_user ON password_reset_tokens(user_id);`);
  } catch (error) {
    console.error('数据库迁移（密码重置Token）失败:', error);
  }

  // 系统日志表
  db.exec(`
    CREATE TABLE IF NOT EXISTS system_logs (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      level TEXT NOT NULL DEFAULT 'info',
      category TEXT NOT NULL,
      action TEXT NOT NULL,
      message TEXT NOT NULL,
      user_id INTEGER,
      username TEXT,
      ip_address TEXT,
      user_agent TEXT,
      details TEXT,
      created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
      FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE SET NULL
    )
  `);

  // 下载流量日统计表（用于用户侧报表）
  db.exec(`
    CREATE TABLE IF NOT EXISTS user_download_traffic_daily (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      user_id INTEGER NOT NULL,
      date_key TEXT NOT NULL, -- YYYY-MM-DD（本地时区）
      bytes_used INTEGER NOT NULL DEFAULT 0,
      download_count INTEGER NOT NULL DEFAULT 0,
      created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
      updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
      UNIQUE(user_id, date_key),
      FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
    )
  `);

  // 下载流量预扣保留表（直连下载签发时占用额度，不计入已用）
  db.exec(`
    CREATE TABLE IF NOT EXISTS user_download_traffic_reservations (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      user_id INTEGER NOT NULL,
      source TEXT NOT NULL DEFAULT 'direct', -- direct/share_direct
      object_key TEXT,
      reserved_bytes INTEGER NOT NULL DEFAULT 0,
      remaining_bytes INTEGER NOT NULL DEFAULT 0,
      status TEXT NOT NULL DEFAULT 'pending', -- pending/confirmed/expired/cancelled
      created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
      expires_at DATETIME NOT NULL,
      finalized_at DATETIME,
      updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
      FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
    )
  `);

  // OSS 日志已处理记录（避免重复入账）
  db.exec(`
    CREATE TABLE IF NOT EXISTS download_traffic_ingested_logs (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      bucket TEXT NOT NULL,
      log_key TEXT NOT NULL,
      etag TEXT,
      file_size INTEGER DEFAULT 0,
      line_count INTEGER DEFAULT 0,
      bytes_count INTEGER DEFAULT 0,
      status TEXT NOT NULL DEFAULT 'success',
      error_message TEXT,
      processed_at DATETIME DEFAULT CURRENT_TIMESTAMP,
      UNIQUE(bucket, log_key, etag)
    )
  `);

  // 本地分片上传会话（断点续传）
  db.exec(`
    CREATE TABLE IF NOT EXISTS upload_sessions (
      session_id TEXT PRIMARY KEY,
      user_id INTEGER NOT NULL,
      storage_type TEXT NOT NULL DEFAULT 'local',
      target_path TEXT NOT NULL,
      file_name TEXT NOT NULL,
      file_size INTEGER NOT NULL DEFAULT 0,
      chunk_size INTEGER NOT NULL DEFAULT 0,
      total_chunks INTEGER NOT NULL DEFAULT 0,
      uploaded_chunks TEXT NOT NULL DEFAULT '',
      uploaded_bytes INTEGER NOT NULL DEFAULT 0,
      temp_file_path TEXT NOT NULL,
      file_hash TEXT,
      status TEXT NOT NULL DEFAULT 'active', -- active/completed/expired/cancelled
      expires_at DATETIME NOT NULL,
      created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
      updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
      completed_at DATETIME,
      FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
    )
  `);

  // 在线设备会话（用于设备管理和强制下线）
  db.exec(`
    CREATE TABLE IF NOT EXISTS user_device_sessions (
      session_id TEXT PRIMARY KEY,
      user_id INTEGER NOT NULL,
      client_type TEXT NOT NULL DEFAULT 'web',
      device_id TEXT,
      device_name TEXT,
      platform TEXT,
      ip_address TEXT,
      user_agent TEXT,
      created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
      last_active_at DATETIME DEFAULT CURRENT_TIMESTAMP,
      expires_at DATETIME NOT NULL,
      revoked_at DATETIME,
      revoked_reason TEXT,
      updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
      FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
    )
  `);

  // 文件哈希索引（用于秒传）
  db.exec(`
    CREATE TABLE IF NOT EXISTS user_file_hash_index (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      user_id INTEGER NOT NULL,
      storage_type TEXT NOT NULL DEFAULT 'local',
      file_hash TEXT NOT NULL,
      file_size INTEGER NOT NULL DEFAULT 0,
      file_path TEXT NOT NULL,
      object_key TEXT,
      created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
      updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
      UNIQUE(user_id, storage_type, file_hash, file_size, file_path),
      FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
    )
  `);

  // 日志表索引
  db.exec(`
    CREATE INDEX IF NOT EXISTS idx_logs_created_at ON system_logs(created_at);
    CREATE INDEX IF NOT EXISTS idx_logs_category ON system_logs(category);
    CREATE INDEX IF NOT EXISTS idx_logs_level ON system_logs(level);
    CREATE INDEX IF NOT EXISTS idx_logs_user_id ON system_logs(user_id);

    -- ===== 性能优化：复合索引（P0 优先级修复） =====
    -- 活动日志复合索引：user_id + created_at
    -- 优势：快速查询用户最近的活动记录，支持时间范围过滤
    -- 使用场景：用户活动历史、审计日志查询
    CREATE INDEX IF NOT EXISTS idx_logs_user_created ON system_logs(user_id, created_at);

    -- 下载流量报表索引（按用户+日期查询）
    CREATE INDEX IF NOT EXISTS idx_download_traffic_user_date ON user_download_traffic_daily(user_id, date_key);

    -- 下载预扣索引（按用户+状态+到期时间）
    CREATE INDEX IF NOT EXISTS idx_download_reservation_user_status_expires
      ON user_download_traffic_reservations(user_id, status, expires_at);

    -- 已处理日志索引（按处理时间回溯）
    CREATE INDEX IF NOT EXISTS idx_ingested_logs_processed_at
      ON download_traffic_ingested_logs(processed_at);

    -- 断点上传会话索引
    CREATE INDEX IF NOT EXISTS idx_upload_sessions_user_status_expires
      ON upload_sessions(user_id, status, expires_at);
    CREATE INDEX IF NOT EXISTS idx_upload_sessions_hash
      ON upload_sessions(user_id, file_hash, file_size);

    -- 在线设备会话索引
    CREATE INDEX IF NOT EXISTS idx_user_device_sessions_user_active
      ON user_device_sessions(user_id, revoked_at, expires_at, last_active_at);
    CREATE INDEX IF NOT EXISTS idx_user_device_sessions_expires
      ON user_device_sessions(expires_at);

    -- 秒传索引
    CREATE INDEX IF NOT EXISTS idx_file_hash_index_lookup
      ON user_file_hash_index(user_id, storage_type, file_hash, file_size);
    CREATE INDEX IF NOT EXISTS idx_file_hash_index_path
      ON user_file_hash_index(user_id, storage_type, file_path);
  `);

  console.log('[数据库性能优化] ✓ 日志表复合索引已创建');
  console.log('  - idx_logs_user_created: 用户+创建时间');

  // 数据库迁移：添加 storage_used 字段（P0 性能优化）
  try {
    const columns = db.prepare("PRAGMA table_info(users)").all();
    const hasStorageUsed = columns.some(col => col.name === 'storage_used');

    if (!hasStorageUsed) {
      db.exec(`ALTER TABLE users ADD COLUMN storage_used INTEGER DEFAULT 0`);
      console.log('[数据库迁移] ✓ storage_used 字段已添加');
    }
  } catch (error) {
    console.error('[数据库迁移] storage_used 字段添加失败:', error);
  }

  console.log('数据库初始化完成');
}

// 创建默认管理员账号
function createDefaultAdmin() {
  const adminExists = db.prepare('SELECT id FROM users WHERE is_admin = 1').get();

  if (!adminExists) {
    // 从环境变量读取管理员账号密码，如果没有则使用默认值
    const adminUsername = process.env.ADMIN_USERNAME || 'admin';
    const adminPassword = process.env.ADMIN_PASSWORD || 'admin123';

    const hashedPassword = bcrypt.hashSync(adminPassword, 10);

    db.prepare(`
      INSERT INTO users (
        username, email, password,
        is_admin, is_active, has_oss_config, is_verified
      ) VALUES (?, ?, ?, ?, ?, ?, ?)
    `).run(
      adminUsername,
      `${adminUsername}@example.com`,
      hashedPassword,
      1,
      1,
      0,  // 管理员不需要OSS配置
      1   // 管理员默认已验证
    );

    console.log('默认管理员账号已创建');
    console.log('用户名:', adminUsername);
    console.log('密码: ********');
    console.log('⚠️  请登录后立即修改密码！');
  }
}

// 用户相关操作
const UserDB = {
  // 创建用户
  create(userData) {
    const hashedPassword = bcrypt.hashSync(userData.password, 10);

    const hasOssConfig = userData.oss_provider && userData.oss_access_key_id && userData.oss_access_key_secret && userData.oss_bucket ? 1 : 0;

    // 对验证令牌进行哈希存储（与 VerificationDB.setVerification 保持一致）
    const hashedVerificationToken = userData.verification_token
      ? crypto.createHash('sha256').update(userData.verification_token).digest('hex')
      : null;

    const stmt = db.prepare(`
      INSERT INTO users (
        username, email, password,
        oss_provider, oss_region, oss_access_key_id, oss_access_key_secret, oss_bucket, oss_endpoint,
        has_oss_config,
        is_verified, verification_token, verification_expires_at
      ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
    `);

    const result = stmt.run(
      userData.username,
      userData.email,
      hashedPassword,
      userData.oss_provider || null,
      userData.oss_region || null,
      userData.oss_access_key_id || null,
      userData.oss_access_key_secret || null,
      userData.oss_bucket || null,
      userData.oss_endpoint || null,
      hasOssConfig,
      userData.is_verified !== undefined ? userData.is_verified : 0,
      hashedVerificationToken,
      userData.verification_expires_at || null
    );

    return result.lastInsertRowid;
  },

  // 根据用户名查找
  findByUsername(username) {
    return db.prepare('SELECT * FROM users WHERE username = ?').get(username);
  },

  // 根据邮箱查找
  findByEmail(email) {
    return db.prepare('SELECT * FROM users WHERE email = ?').get(email);
  },

  // 根据ID查找
  findById(id) {
    return db.prepare('SELECT * FROM users WHERE id = ?').get(id);
  },

  // 验证密码
  verifyPassword(plainPassword, hashedPassword) {
    return bcrypt.compareSync(plainPassword, hashedPassword);
  },

  /**
   * 字段类型验证函数
   * 确保所有字段值类型符合数据库要求
   * @param {string} fieldName - 字段名
   * @param {*} value - 字段值
   * @returns {boolean} 是否有效
   * @private
   */
  _validateFieldValue(fieldName, value) {
    const NULLABLE_STRING_FIELDS = new Set([
      'oss_provider',
      'oss_region',
      'oss_access_key_id',
      'oss_access_key_secret',
      'oss_bucket',
      'oss_endpoint',
      'upload_api_key',
      'verification_token',
      'verification_expires_at',
      'storage_permission',
      'current_storage_type',
      'download_traffic_quota_expires_at',
      'download_traffic_reset_cycle',
      'download_traffic_last_reset_at',
      'theme_preference'
    ]);

    // 字段类型白名单（根据数据库表结构定义）
    const FIELD_TYPES = {
      // 文本类型字段
      'username': 'string',
      'email': 'string',
      'password': 'string',
      'oss_provider': 'string',
      'oss_region': 'string',
      'oss_access_key_id': 'string',
      'oss_access_key_secret': 'string',
      'oss_bucket': 'string',
      'oss_endpoint': 'string',
      'upload_api_key': 'string',
      'verification_token': 'string',
      'verification_expires_at': 'string',
      'storage_permission': 'string',
      'current_storage_type': 'string',
      'download_traffic_quota_expires_at': 'string',
      'download_traffic_reset_cycle': 'string',
      'download_traffic_last_reset_at': 'string',
      'theme_preference': 'string',

      // 数值类型字段
      'is_admin': 'number',
      'is_active': 'number',
      'is_banned': 'is_banned',
      'has_oss_config': 'number',
      'is_verified': 'number',
      'local_storage_quota': 'number',
      'local_storage_used': 'number',
      'oss_storage_quota': 'number',
      'download_traffic_quota': 'number',
      'download_traffic_used': 'number'
    };

    const expectedType = FIELD_TYPES[fieldName];

    // 如果字段不在类型定义中，允许通过（向后兼容）
    if (!expectedType) {
      return true;
    }

    // 检查类型匹配
    if (expectedType === 'string') {
      if (value === null) {
        return NULLABLE_STRING_FIELDS.has(fieldName);
      }
      return typeof value === 'string';
    } else if (expectedType === 'number') {
      // 允许数值或可转换为数值的字符串
      return typeof value === 'number' || (typeof value === 'string' && !isNaN(Number(value)));
    }

    return true;
  },

  /**
   * 验证字段映射完整性
   * 确保 FIELD_MAP 中定义的所有字段都在数据库表中存在
   * @returns {Object} 验证结果 { valid: boolean, missing: string[], extra: string[] }
   * @private
   */
  _validateFieldMapping() {
    // 字段映射白名单：防止别名攻击（如 toString、valueOf 等原型方法）
    const FIELD_MAP = {
      // 基础字段
      'username': 'username',
      'email': 'email',
      'password': 'password',

      // OSS 配置字段
      'oss_provider': 'oss_provider',
      'oss_region': 'oss_region',
      'oss_access_key_id': 'oss_access_key_id',
      'oss_access_key_secret': 'oss_access_key_secret',
      'oss_bucket': 'oss_bucket',
      'oss_endpoint': 'oss_endpoint',

      // API 密钥和权限字段
      'upload_api_key': 'upload_api_key',
      'is_admin': 'is_admin',
      'is_active': 'is_active',
      'is_banned': 'is_banned',
      'has_oss_config': 'has_oss_config',

      // 验证字段
      'is_verified': 'is_verified',
      'verification_token': 'verification_token',
      'verification_expires_at': 'verification_expires_at',

      // 存储配置字段
      'storage_permission': 'storage_permission',
      'current_storage_type': 'current_storage_type',
      'local_storage_quota': 'local_storage_quota',
      'local_storage_used': 'local_storage_used',
      'oss_storage_quota': 'oss_storage_quota',
      'download_traffic_quota': 'download_traffic_quota',
      'download_traffic_used': 'download_traffic_used',
      'download_traffic_quota_expires_at': 'download_traffic_quota_expires_at',
      'download_traffic_reset_cycle': 'download_traffic_reset_cycle',
      'download_traffic_last_reset_at': 'download_traffic_last_reset_at',

      // 偏好设置
      'theme_preference': 'theme_preference'
    };

    try {
      // 获取数据库表的实际列信息
      const columns = db.prepare("PRAGMA table_info(users)").all();
      const dbFields = new Set(columns.map(col => col.name));

      // 检查 FIELD_MAP 中的字段是否都在数据库中存在
      const mappedFields = new Set(Object.values(FIELD_MAP));
      const missingFields = [];
      const extraFields = [];

      for (const field of mappedFields) {
        if (!dbFields.has(field)) {
          missingFields.push(field);
        }
      }

      // 检查数据库中是否有 FIELD_MAP 未定义的字段（可选）
      for (const dbField of dbFields) {
        if (!mappedFields.has(dbField) && !['id', 'created_at', 'updated_at'].includes(dbField)) {
          extraFields.push(dbField);
        }
      }

      const isValid = missingFields.length === 0;

      if (!isValid) {
        console.error(`[数据库错误] 字段映射验证失败，缺失字段: ${missingFields.join(', ')}`);
      }

      if (extraFields.length > 0) {
        console.warn(`[数据库警告] 数据库存在 FIELD_MAP 未定义的字段: ${extraFields.join(', ')}`);
      }

      return { valid: isValid, missing: missingFields, extra: extraFields };
    } catch (error) {
      console.error(`[数据库错误] 字段映射验证失败: ${error.message}`);
      return { valid: false, missing: [], extra: [], error: error.message };
    }
  },

  // 更新用户
  // 安全修复：使用字段映射白名单，防止 SQL 注入和原型污染攻击
  update(id, updates) {
    // 字段映射白名单：防止别名攻击（如 toString、valueOf 等原型方法）
    const FIELD_MAP = {
      // 基础字段
      'username': 'username',
      'email': 'email',
      'password': 'password',

      // OSS 配置字段
      'oss_provider': 'oss_provider',
      'oss_region': 'oss_region',
      'oss_access_key_id': 'oss_access_key_id',
      'oss_access_key_secret': 'oss_access_key_secret',
      'oss_bucket': 'oss_bucket',
      'oss_endpoint': 'oss_endpoint',

      // API 密钥和权限字段
      'upload_api_key': 'upload_api_key',
      'is_admin': 'is_admin',
      'is_active': 'is_active',
      'is_banned': 'is_banned',
      'has_oss_config': 'has_oss_config',

      // 验证字段
      'is_verified': 'is_verified',
      'verification_token': 'verification_token',
      'verification_expires_at': 'verification_expires_at',

      // 存储配置字段
      'storage_permission': 'storage_permission',
      'current_storage_type': 'current_storage_type',
      'local_storage_quota': 'local_storage_quota',
      'local_storage_used': 'local_storage_used',
      'oss_storage_quota': 'oss_storage_quota',
      'download_traffic_quota': 'download_traffic_quota',
      'download_traffic_used': 'download_traffic_used',
      'download_traffic_quota_expires_at': 'download_traffic_quota_expires_at',
      'download_traffic_reset_cycle': 'download_traffic_reset_cycle',
      'download_traffic_last_reset_at': 'download_traffic_last_reset_at',

      // 偏好设置
      'theme_preference': 'theme_preference'
    };

    const fields = [];
    const values = [];
    const rejectedFields = []; // 记录被拒绝的字段（类型不符）

    for (const [key, value] of Object.entries(updates)) {
      // 安全检查 1：确保是对象自身的属性（防止原型污染）
      // 使用 Object.prototype.hasOwnProperty.call() 避免原型链污染
      if (!Object.prototype.hasOwnProperty.call(updates, key)) {
        console.warn(`[安全警告] 跳过非自身属性: ${key} (类型: ${typeof key})`);
        continue;
      }

      // 安全检查 2：字段名必须是字符串类型
      if (typeof key !== 'string' || key.trim() === '') {
        console.warn(`[安全警告] 跳过无效字段名: ${key} (类型: ${typeof key})`);
        rejectedFields.push({ field: key, reason: '字段名不是有效字符串' });
        continue;
      }

      // 安全检查 3：验证字段映射（防止别名攻击）
      const mappedField = FIELD_MAP[key];
      if (!mappedField) {
        console.warn(`[安全警告] 尝试更新非法字段: ${key}`);
        rejectedFields.push({ field: key, reason: '字段不在白名单中' });
        continue;
      }

      // 安全检查 4：确保字段名不包含特殊字符或 SQL 关键字
      // 只允许字母、数字和下划线
      if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(mappedField)) {
        console.warn(`[安全警告] 字段名包含非法字符: ${mappedField}`);
        rejectedFields.push({ field: key, reason: '字段名包含非法字符' });
        continue;
      }

      // 安全检查 5：验证字段值类型（第二轮修复）
      if (!this._validateFieldValue(key, value)) {
        const expectedType = {
          'username': 'string', 'email': 'string', 'password': 'string',
          'oss_provider': 'string', 'oss_region': 'string',
          'oss_access_key_id': 'string', 'oss_access_key_secret': 'string',
          'oss_bucket': 'string', 'oss_endpoint': 'string',
          'upload_api_key': 'string', 'verification_token': 'string',
          'verification_expires_at': 'string', 'storage_permission': 'string',
          'current_storage_type': 'string', 'theme_preference': 'string',
          'download_traffic_quota_expires_at': 'string',
          'download_traffic_reset_cycle': 'string',
          'download_traffic_last_reset_at': 'string',
          'is_admin': 'number', 'is_active': 'number', 'is_banned': 'number',
          'has_oss_config': 'number', 'is_verified': 'number',
          'local_storage_quota': 'number', 'local_storage_used': 'number',
          'oss_storage_quota': 'number', 'download_traffic_quota': 'number',
          'download_traffic_used': 'number'
        }[key];

        console.warn(`[类型检查] 字段 ${key} 值类型不符: 期望 ${expectedType}, 实际 ${typeof value}, 值: ${JSON.stringify(value)}`);
        rejectedFields.push({ field: key, reason: `值类型不符 (期望: ${expectedType}, 实际: ${typeof value})` });
        continue;
      }

      // 特殊处理密码字段（需要哈希）
      if (key === 'password') {
        fields.push(`${mappedField} = ?`);
        values.push(bcrypt.hashSync(value, 10));
      } else {
        fields.push(`${mappedField} = ?`);
        values.push(value);
      }
    }

    // 记录被拒绝的字段（用于调试）
    if (rejectedFields.length > 0) {
      console.log(`[类型检查] 用户 ${id} 更新请求拒绝了 ${rejectedFields.length} 个字段:`, rejectedFields);
    }

    // 如果没有有效字段，返回空结果
    if (fields.length === 0) {
      console.warn(`[安全警告] 没有有效字段可更新，用户ID: ${id}`);
      return { changes: 0, rejectedFields };
    }

    // 添加 updated_at 时间戳
    fields.push('updated_at = CURRENT_TIMESTAMP');
    values.push(id);

    // 使用参数化查询执行更新（防止 SQL 注入）
    const stmt = db.prepare(`UPDATE users SET ${fields.join(', ')} WHERE id = ?`);
    const result = stmt.run(...values);

    // 附加被拒绝字段信息到返回结果
    result.rejectedFields = rejectedFields;
    return result;
  },

  // 获取所有用户
  getAll(filters = {}) {
    let query = 'SELECT * FROM users WHERE 1=1';
    const params = [];

    if (filters.is_admin !== undefined) {
      query += ' AND is_admin = ?';
      params.push(filters.is_admin);
    }

    if (filters.is_banned !== undefined) {
      query += ' AND is_banned = ?';
      params.push(filters.is_banned);
    }

    query += ' ORDER BY created_at DESC';

    return db.prepare(query).all(...params);
  },

  queryAdminUsers(options = {}) {
    const parsedPage = Number.parseInt(options.page, 10);
    const parsedPageSize = Number.parseInt(options.pageSize, 10);
    const safePage = Number.isFinite(parsedPage) && parsedPage > 0 ? parsedPage : 1;
    const safePageSize = Number.isFinite(parsedPageSize)
      ? Math.min(200, Math.max(1, parsedPageSize))
      : 20;

    const keyword = typeof options.keyword === 'string' ? options.keyword.trim() : '';
    const role = ['all', 'admin', 'user'].includes(options.role) ? options.role : 'all';
    const status = ['all', 'active', 'banned', 'unverified', 'download_blocked'].includes(options.status)
      ? options.status
      : 'all';
    const storage = ['all', 'local', 'oss', 'local_only', 'oss_only', 'user_choice'].includes(options.storage)
      ? options.storage
      : 'all';
    const sort = [
      'created_desc',
      'created_asc',
      'username_asc',
      'username_desc',
      'storage_usage_desc',
      'download_usage_desc'
    ].includes(options.sort)
      ? options.sort
      : 'created_desc';

    const whereClauses = ['1=1'];
    const params = [];

    if (keyword) {
      const likeKeyword = `%${keyword}%`;
      whereClauses.push('(CAST(id AS TEXT) LIKE ? OR username LIKE ? OR email LIKE ?)');
      params.push(likeKeyword, likeKeyword, likeKeyword);
    }

    if (role === 'admin') {
      whereClauses.push('is_admin = 1');
    } else if (role === 'user') {
      whereClauses.push('is_admin = 0');
    }

    if (status === 'banned') {
      whereClauses.push('is_banned = 1');
    } else if (status === 'unverified') {
      whereClauses.push('is_banned = 0 AND is_verified = 0');
    } else if (status === 'download_blocked') {
      whereClauses.push(`
        is_banned = 0
        AND is_verified = 1
        AND COALESCE(download_traffic_quota, 0) >= 0
        AND (
          COALESCE(download_traffic_quota, 0) = 0
          OR COALESCE(download_traffic_used, 0) >= COALESCE(download_traffic_quota, 0)
        )
      `);
    } else if (status === 'active') {
      whereClauses.push(`
        is_banned = 0
        AND is_verified = 1
        AND (
          COALESCE(download_traffic_quota, -1) < 0
          OR (
            COALESCE(download_traffic_quota, 0) > 0
            AND COALESCE(download_traffic_used, 0) < COALESCE(download_traffic_quota, 0)
          )
        )
      `);
    }

    if (storage === 'local' || storage === 'oss') {
      whereClauses.push("COALESCE(current_storage_type, 'oss') = ?");
      params.push(storage);
    } else if (storage !== 'all') {
      whereClauses.push("COALESCE(storage_permission, 'oss_only') = ?");
      params.push(storage);
    }

    const defaultOssQuota = 1024 * 1024 * 1024; // 1GB
    const storageUsageExpr = `
      CASE
        WHEN COALESCE(current_storage_type, 'oss') = 'local' THEN
          CASE
            WHEN COALESCE(local_storage_quota, 0) > 0
              THEN (COALESCE(local_storage_used, 0) * 100.0 / COALESCE(local_storage_quota, 1))
            ELSE 0
          END
        ELSE
          CASE
            WHEN COALESCE(oss_storage_quota, 0) > 0
              THEN (COALESCE(storage_used, 0) * 100.0 / COALESCE(oss_storage_quota, ${defaultOssQuota}))
            ELSE 0
          END
      END
    `;
    const downloadUsageExpr = `
      CASE
        WHEN COALESCE(download_traffic_quota, 0) < 0 THEN 0
        WHEN COALESCE(download_traffic_quota, 0) = 0 THEN 100
        ELSE MIN(
          100,
          COALESCE(download_traffic_used, 0) * 100.0 / MAX(COALESCE(download_traffic_quota, 0), 1)
        )
      END
    `;

    const orderByMap = {
      created_desc: 'datetime(created_at) DESC, id DESC',
      created_asc: 'datetime(created_at) ASC, id ASC',
      username_asc: "LOWER(COALESCE(username, '')) ASC, id ASC",
      username_desc: "LOWER(COALESCE(username, '')) DESC, id DESC",
      storage_usage_desc: `${storageUsageExpr} DESC, id DESC`,
      download_usage_desc: `${downloadUsageExpr} DESC, id DESC`
    };
    const orderBySql = orderByMap[sort] || orderByMap.created_desc;
    const whereSql = whereClauses.join(' AND ');

    const globalTotalRow = db.prepare('SELECT COUNT(*) AS total FROM users').get();
    const globalTotal = Number(globalTotalRow?.total || 0);

    const totalRow = db.prepare(`
      SELECT COUNT(*) AS total
      FROM users
      WHERE ${whereSql}
    `).get(...params);
    const filteredTotal = Number(totalRow?.total || 0);

    const summaryRow = db.prepare(`
      SELECT
        COUNT(*) AS filtered_total,
        SUM(CASE WHEN is_banned = 1 THEN 1 ELSE 0 END) AS banned,
        SUM(CASE WHEN is_banned = 0 AND is_verified = 0 THEN 1 ELSE 0 END) AS unverified,
        SUM(
          CASE
            WHEN is_banned = 0
             AND is_verified = 1
             AND COALESCE(download_traffic_quota, 0) >= 0
             AND (
               COALESCE(download_traffic_quota, 0) = 0
               OR COALESCE(download_traffic_used, 0) >= COALESCE(download_traffic_quota, 0)
             )
            THEN 1 ELSE 0
          END
        ) AS download_blocked,
        SUM(
          CASE
            WHEN is_banned = 0
             AND is_verified = 1
             AND (
               COALESCE(download_traffic_quota, -1) < 0
               OR (
                 COALESCE(download_traffic_quota, 0) > 0
                 AND COALESCE(download_traffic_used, 0) < COALESCE(download_traffic_quota, 0)
               )
             )
            THEN 1 ELSE 0
          END
        ) AS active
      FROM users
      WHERE ${whereSql}
    `).get(...params);

    const totalPages = Math.max(1, Math.ceil(filteredTotal / safePageSize));
    const page = Math.min(Math.max(1, safePage), totalPages);
    const offset = (page - 1) * safePageSize;

    const rows = db.prepare(`
      SELECT *
      FROM users
      WHERE ${whereSql}
      ORDER BY ${orderBySql}
      LIMIT ? OFFSET ?
    `).all(...params, safePageSize, offset);

    return {
      rows,
      pagination: {
        page,
        pageSize: safePageSize,
        total: filteredTotal,
        totalPages
      },
      summary: {
        global_total: Number.isFinite(globalTotal) ? globalTotal : 0,
        filtered_total: Number(summaryRow?.filtered_total || 0),
        active: Number(summaryRow?.active || 0),
        banned: Number(summaryRow?.banned || 0),
        unverified: Number(summaryRow?.unverified || 0),
        download_blocked: Number(summaryRow?.download_blocked || 0)
      }
    };
  },

  // 删除用户
  delete(id) {
    return db.prepare('DELETE FROM users WHERE id = ?').run(id);
  },

  // 封禁/解封用户
  setBanStatus(id, isBanned) {
    return db.prepare('UPDATE users SET is_banned = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?')
      .run(isBanned ? 1 : 0, id);
  }
};

// 分享链接相关操作
const ShareDB = {
  // 生成随机分享码
  generateShareCode(length = 8) {
    const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
    const bytes = crypto.randomBytes(length);
    let code = '';
    for (let i = 0; i < length; i++) {
      code += chars[bytes[i] % chars.length];
    }
    return code;
  },

  // 创建分享链接
  // 创建分享链接
  create(userId, options = {}) {
    const {
      share_type = 'file',
      file_path = '',
      file_name = '',
      password = null,
      expiry_days = null,
      max_downloads = null,
      ip_whitelist = null,
      device_limit = 'all',
      access_time_start = null,
      access_time_end = null
    } = options;

    let shareCode;
    let attempts = 0;

    // 尝试生成唯一的分享码
    do {
      shareCode = this.generateShareCode();
      attempts++;
      if (attempts > 10) {
        shareCode = this.generateShareCode(10); // 增加长度
      }
    } while (this.findByCode(shareCode) && attempts < 20);

    // 计算过期时间
    let expiresAt = null;
    if (expiry_days) {
      const expireDate = new Date();
      expireDate.setDate(expireDate.getDate() + parseInt(expiry_days));
      // 使用本地时区时间,而不是UTC时间
      // 这样前端解析时会正确显示为本地时间
      const year = expireDate.getFullYear();
      const month = String(expireDate.getMonth() + 1).padStart(2, '0');
      const day = String(expireDate.getDate()).padStart(2, '0');
      const hours = String(expireDate.getHours()).padStart(2, '0');
      const minutes = String(expireDate.getMinutes()).padStart(2, '0');
      const seconds = String(expireDate.getSeconds()).padStart(2, '0');
      expiresAt = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
    }

    const stmt = db.prepare(`
      INSERT INTO shares (
        user_id, share_code, share_path, share_type, share_password, expires_at,
        max_downloads, ip_whitelist, device_limit, access_time_start, access_time_end
      )
      VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
    `);

    const hashedPassword = password ? bcrypt.hashSync(password, 10) : null;

    // 修复：正确处理不同类型的分享路径
    let sharePath;
    if (share_type === 'file') {
      // 单文件分享：使用完整文件路径
      sharePath = file_path;
    } else if (share_type === 'directory') {
      // 文件夹分享：使用文件夹路径
      sharePath = file_path;
    } else {
      // all类型：分享根目录
      sharePath = '/';
    }

    const result = stmt.run(
      userId, 
      shareCode, 
      sharePath, 
      share_type,
      hashedPassword,
      expiresAt,
      max_downloads,
      ip_whitelist,
      device_limit || 'all',
      access_time_start,
      access_time_end
    );

    return {
      id: result.lastInsertRowid,
      share_code: shareCode,
      share_type: share_type,
      expires_at: expiresAt,
      max_downloads: max_downloads ?? null,
      ip_whitelist: ip_whitelist ?? null,
      device_limit: device_limit || 'all',
      access_time_start: access_time_start ?? null,
      access_time_end: access_time_end ?? null
    };
  },

  // 根据分享码查找
  // 增强: 检查分享者是否被封禁（被封禁用户的分享不可访问）
  // ===== 性能优化（P0 优先级修复）：只查询必要字段，避免 N+1 查询 =====
  // 不返回 oss_access_key_id / oss_access_key_secret；
  // share_password 仅用于服务端验证，不会透传给前端。
  findByCode(shareCode) {
    const result = db.prepare(`
      SELECT
        s.id, s.user_id, s.share_code, s.share_path, s.share_type,
        s.storage_type,
        s.share_password,
        s.max_downloads, s.ip_whitelist, s.device_limit, s.access_time_start, s.access_time_end,
        s.view_count, s.download_count, s.created_at, s.expires_at,
        u.username,
        -- OSS 配置（访问分享文件所需）
        u.oss_provider, u.oss_region, u.oss_bucket, u.oss_endpoint,
        -- 用户偏好（主题）
        u.theme_preference,
        -- 安全检查
        u.is_banned
      FROM shares s
      JOIN users u ON s.user_id = u.id
      WHERE s.share_code = ?
        AND (s.expires_at IS NULL OR s.expires_at > datetime('now', 'localtime'))
        AND u.is_banned = 0
    `).get(shareCode);

    return result;
  },

  // 根据ID查找
  findById(id) {
    return db.prepare('SELECT * FROM shares WHERE id = ?').get(id);
  },

  // 按目标路径查找已存在分享（用于复用）
  findExistingByTarget(userId, options = {}) {
    const {
      share_type = 'file',
      file_path = '',
      storage_type = 'oss'
    } = options;

    return db.prepare(`
      SELECT *
      FROM shares
      WHERE user_id = ?
        AND share_type = ?
        AND share_path = ?
        AND COALESCE(storage_type, 'oss') = ?
      ORDER BY id DESC
      LIMIT 1
    `).get(
      userId,
      share_type,
      file_path,
      storage_type || 'oss'
    );
  },

  // 验证分享密码
  verifyPassword(plainPassword, hashedPassword) {
    return bcrypt.compareSync(plainPassword, hashedPassword);
  },

  // 获取用户的所有分享
  getUserShares(userId) {
    return db.prepare(`
      SELECT * FROM shares
      WHERE user_id = ?
      ORDER BY created_at DESC
    `).all(userId);
  },

  // 增加查看次数
  incrementViewCount(shareCode) {
    return db.prepare(`
      UPDATE shares
      SET view_count = view_count + 1
      WHERE share_code = ?
    `).run(shareCode);
  },

  // 增加下载次数
  incrementDownloadCount(shareCode) {
    return db.prepare(`
      UPDATE shares
      SET download_count = download_count + 1
      WHERE share_code = ?
    `).run(shareCode);
  },

  // 删除分享
  delete(id, userId = null) {
    if (userId) {
      return db.prepare('DELETE FROM shares WHERE id = ? AND user_id = ?').run(id, userId);
    }
    return db.prepare('DELETE FROM shares WHERE id = ?').run(id);
  },

  // 获取所有分享（管理员）
  getAll() {
    return db.prepare(`
      SELECT s.*, u.username
      FROM shares s
      JOIN users u ON s.user_id = u.id
      ORDER BY s.created_at DESC
    `).all();
  }
};

// 分享直链相关操作（与 ShareDB 独立）
const DirectLinkDB = {
  // 生成随机直链码
  generateLinkCode(length = 10) {
    const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
    const bytes = crypto.randomBytes(length);
    let code = '';
    for (let i = 0; i < length; i++) {
      code += chars[bytes[i] % chars.length];
    }
    return code;
  },

  create(userId, options = {}) {
    const {
      file_path = '',
      file_name = '',
      storage_type = 'oss',
      expiry_days = null
    } = options;

    let linkCode;
    let attempts = 0;
    do {
      linkCode = this.generateLinkCode();
      attempts++;
      if (attempts > 10) {
        linkCode = this.generateLinkCode(14);
      }
    } while (
      db.prepare('SELECT 1 FROM direct_links WHERE link_code = ?').get(linkCode)
      && attempts < 20
    );

    let expiresAt = null;
    if (expiry_days) {
      const expireDate = new Date();
      expireDate.setDate(expireDate.getDate() + parseInt(expiry_days, 10));
      const year = expireDate.getFullYear();
      const month = String(expireDate.getMonth() + 1).padStart(2, '0');
      const day = String(expireDate.getDate()).padStart(2, '0');
      const hours = String(expireDate.getHours()).padStart(2, '0');
      const minutes = String(expireDate.getMinutes()).padStart(2, '0');
      const seconds = String(expireDate.getSeconds()).padStart(2, '0');
      expiresAt = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
    }

    const result = db.prepare(`
      INSERT INTO direct_links (user_id, link_code, file_path, file_name, storage_type, expires_at)
      VALUES (?, ?, ?, ?, ?, ?)
    `).run(
      userId,
      linkCode,
      file_path,
      file_name || null,
      storage_type || 'oss',
      expiresAt
    );

    return {
      id: result.lastInsertRowid,
      link_code: linkCode,
      file_path,
      file_name: file_name || null,
      storage_type: storage_type || 'oss',
      expires_at: expiresAt
    };
  },

  findByCode(linkCode) {
    return db.prepare(`
      SELECT
        dl.*,
        u.username,
        u.is_banned
      FROM direct_links dl
      JOIN users u ON dl.user_id = u.id
      WHERE dl.link_code = ?
        AND (dl.expires_at IS NULL OR dl.expires_at > datetime('now', 'localtime'))
        AND u.is_banned = 0
    `).get(linkCode);
  },

  findById(id) {
    return db.prepare('SELECT * FROM direct_links WHERE id = ?').get(id);
  },

  // 按目标路径查找已存在直链（用于复用）
  findExistingByTarget(userId, options = {}) {
    const {
      file_path = '',
      storage_type = 'oss'
    } = options;

    return db.prepare(`
      SELECT *
      FROM direct_links
      WHERE user_id = ?
        AND file_path = ?
        AND COALESCE(storage_type, 'oss') = ?
      ORDER BY id DESC
      LIMIT 1
    `).get(
      userId,
      file_path,
      storage_type || 'oss'
    );
  },

  getUserLinks(userId) {
    return db.prepare(`
      SELECT *
      FROM direct_links
      WHERE user_id = ?
      ORDER BY created_at DESC
    `).all(userId);
  },

  incrementDownloadCount(linkCode) {
    return db.prepare(`
      UPDATE direct_links
      SET download_count = download_count + 1,
          last_accessed_at = CURRENT_TIMESTAMP
      WHERE link_code = ?
    `).run(linkCode);
  },

  touchAccess(linkCode) {
    return db.prepare(`
      UPDATE direct_links
      SET last_accessed_at = CURRENT_TIMESTAMP
      WHERE link_code = ?
    `).run(linkCode);
  },

  delete(id, userId = null) {
    if (userId) {
      return db.prepare('DELETE FROM direct_links WHERE id = ? AND user_id = ?').run(id, userId);
    }
    return db.prepare('DELETE FROM direct_links WHERE id = ?').run(id);
  }
};

// 系统设置管理
const SettingsDB = {
  // 获取设置
  get(key) {
    const row = db.prepare('SELECT value FROM system_settings WHERE key = ?').get(key);
    return row ? row.value : null;
  },

  // 设置值
  set(key, value) {
    db.prepare(`
      INSERT INTO system_settings (key, value, updated_at)
      VALUES (?, ?, CURRENT_TIMESTAMP)
      ON CONFLICT(key) DO UPDATE SET
        value = excluded.value,
        updated_at = CURRENT_TIMESTAMP
    `).run(key, value);
  },

  // 获取所有设置
  getAll() {
    return db.prepare('SELECT key, value FROM system_settings').all();
  },

  // ===== 统一 OSS 配置管理（管理员配置，所有用户共享） =====

  /**
   * 获取统一的 OSS 配置
   * @returns {Object|null} OSS 配置对象，如果未配置则返回 null
   */
  getUnifiedOssConfig() {
    const config = {
      provider: this.get('oss_provider'),
      region: this.get('oss_region'),
      access_key_id: this.get('oss_access_key_id'),
      access_key_secret: this.get('oss_access_key_secret'),
      bucket: this.get('oss_bucket'),
      endpoint: this.get('oss_endpoint')
    };

    // 检查是否所有必需字段都已配置
    if (!config.provider || !config.access_key_id || !config.access_key_secret || !config.bucket) {
      return null;
    }

    // 安全修复：解密 OSS Access Key Secret
    try {
      if (config.access_key_secret) {
        config.access_key_secret = decryptSecret(config.access_key_secret);
      }
    } catch (error) {
      console.error('[安全] 解密统一 OSS 配置失败:', error.message);
      return null;
    }

    return config;
  },

  /**
   * 设置统一的 OSS 配置
   * @param {Object} ossConfig - OSS 配置对象
   * @param {string} ossConfig.provider - 服务商（aliyun/tencent/aws）
   * @param {string} ossConfig.region - 区域
   * @param {string} ossConfig.access_key_id - Access Key ID
   * @param {string} ossConfig.access_key_secret - Access Key Secret
   * @param {string} ossConfig.bucket - 存储桶名称
   * @param {string} [ossConfig.endpoint] - 自定义 Endpoint（可选）
   */
  setUnifiedOssConfig(ossConfig) {
    this.set('oss_provider', ossConfig.provider);
    this.set('oss_region', ossConfig.region);
    this.set('oss_access_key_id', ossConfig.access_key_id);

    // 安全修复：加密存储 OSS Access Key Secret
    try {
      const encryptedSecret = encryptSecret(ossConfig.access_key_secret);
      this.set('oss_access_key_secret', encryptedSecret);
    } catch (error) {
      console.error('[安全] 加密统一 OSS 配置失败:', error.message);
      throw new Error('保存 OSS 配置失败：加密错误');
    }

    this.set('oss_bucket', ossConfig.bucket);
    this.set('oss_endpoint', ossConfig.endpoint || '');
    console.log('[系统设置] 统一 OSS 配置已更新（已加密）');
  },

  /**
   * 删除统一的 OSS 配置
   */
  clearUnifiedOssConfig() {
    db.prepare('DELETE FROM system_settings WHERE key LIKE "oss_%"').run();
    console.log('[系统设置] 统一 OSS 配置已清除');
  },

  /**
   * 检查是否已配置统一的 OSS
   * @returns {boolean}
   */
  hasUnifiedOssConfig() {
    return this.getUnifiedOssConfig() !== null;
  }
};

// 邮箱验证管理（增强安全：哈希存储）
const VerificationDB = {
  setVerification(userId, token, expiresAtMs) {
    // 对令牌进行哈希后存储
    const hashedToken = crypto.createHash('sha256').update(token).digest('hex');
    db.prepare(`
      UPDATE users
      SET verification_token = ?, verification_expires_at = ?, is_verified = 0, updated_at = CURRENT_TIMESTAMP
      WHERE id = ?
    `).run(hashedToken, expiresAtMs, userId);
  },
  consumeVerificationToken(token) {
    // 对用户提供的令牌进行哈希
    const hashedToken = crypto.createHash('sha256').update(token).digest('hex');
    const row = db.prepare(`
      SELECT * FROM users
      WHERE verification_token = ?
        AND (
          verification_expires_at IS NULL
          OR verification_expires_at = ''
          OR verification_expires_at > strftime('%s','now')*1000  -- 数值时间戳（ms）
          OR verification_expires_at > CURRENT_TIMESTAMP         -- 兼容旧的字符串时间
        )
        AND is_verified = 0
    `).get(hashedToken);
    if (!row) return null;

    db.prepare(`
      UPDATE users
      SET is_verified = 1, verification_token = NULL, verification_expires_at = NULL, updated_at = CURRENT_TIMESTAMP
      WHERE id = ?
    `).run(row.id);
    return row;
  }
};

// 密码重置 Token 管理（增强安全：哈希存储）
const PasswordResetTokenDB = {
  // 创建令牌时存储哈希值
  create(userId, token, expiresAtMs) {
    // 对令牌进行哈希后存储（防止数据库泄露时令牌被直接使用）
    const hashedToken = crypto.createHash('sha256').update(token).digest('hex');
    db.prepare(`
      INSERT INTO password_reset_tokens (user_id, token, expires_at, used)
      VALUES (?, ?, ?, 0)
    `).run(userId, hashedToken, expiresAtMs);
  },
  // 验证令牌时先哈希再比较
  use(token) {
    // 对用户提供的令牌进行哈希
    const hashedToken = crypto.createHash('sha256').update(token).digest('hex');
    const row = db.prepare(`
      SELECT * FROM password_reset_tokens
      WHERE token = ? AND used = 0 AND (
        expires_at > strftime('%s','now')*1000  -- 数值时间戳
        OR expires_at > CURRENT_TIMESTAMP      -- 兼容旧的字符串时间
      )
    `).get(hashedToken);
    if (!row) return null;
    // 立即标记为已使用（防止重复使用）
    db.prepare(`UPDATE password_reset_tokens SET used = 1 WHERE id = ?`).run(row.id);
    return row;
  }
};

// 初始化默认设置
function initDefaultSettings() {
  // 默认上传限制为10GB
  if (!SettingsDB.get('max_upload_size')) {
    SettingsDB.set('max_upload_size', '10737418240'); // 10GB in bytes
  }
  // 默认全局主题为暗色
  if (!SettingsDB.get('global_theme')) {
    SettingsDB.set('global_theme', 'dark');
  }

  // 下载安全策略默认值（同IP同文件限速：5分钟3次、1小时10次、1天20次）
  if (!SettingsDB.get('download_security_enabled')) {
    SettingsDB.set('download_security_enabled', 'true');
  }
  if (!SettingsDB.get('download_security_same_file_enabled')) {
    SettingsDB.set('download_security_same_file_enabled', 'true');
  }
  if (!SettingsDB.get('download_security_same_file_limit_5m')) {
    SettingsDB.set('download_security_same_file_limit_5m', '3');
  }
  if (!SettingsDB.get('download_security_same_file_limit_1h')) {
    SettingsDB.set('download_security_same_file_limit_1h', '10');
  }
  if (!SettingsDB.get('download_security_same_file_limit_1d')) {
    SettingsDB.set('download_security_same_file_limit_1d', '20');
  }

  // 扩展策略默认关闭（管理员可按需开启）
  if (!SettingsDB.get('download_security_same_user_enabled')) {
    SettingsDB.set('download_security_same_user_enabled', 'false');
  }
  if (!SettingsDB.get('download_security_same_user_limit_1h')) {
    SettingsDB.set('download_security_same_user_limit_1h', '80');
  }
  if (!SettingsDB.get('download_security_same_user_limit_1d')) {
    SettingsDB.set('download_security_same_user_limit_1d', '300');
  }
  if (!SettingsDB.get('download_security_same_file_min_interval_enabled')) {
    SettingsDB.set('download_security_same_file_min_interval_enabled', 'false');
  }
  if (!SettingsDB.get('download_security_same_file_min_interval_seconds')) {
    SettingsDB.set('download_security_same_file_min_interval_seconds', '2');
  }
}

// 数据库迁移 - 主题偏好字段
function migrateThemePreference() {
  try {
    const columns = db.prepare("PRAGMA table_info(users)").all();
    const hasThemePreference = columns.some(col => col.name === 'theme_preference');

    if (!hasThemePreference) {
      console.log('[数据库迁移] 添加主题偏好字段...');
      db.exec(`ALTER TABLE users ADD COLUMN theme_preference TEXT DEFAULT NULL`);
      console.log('[数据库迁移] ✓ 主题偏好字段已添加');
    }
  } catch (error) {
    console.error('[数据库迁移] 主题偏好迁移失败:', error);
  }
}

// 数据库版本迁移 - v2.0 本地存储功能
function migrateToV2() {
  try {
    const columns = db.prepare("PRAGMA table_info(users)").all();
    const hasStoragePermission = columns.some(col => col.name === 'storage_permission');

    if (!hasStoragePermission) {
      console.log('[数据库迁移] 检测到旧版本数据库，开始升级到 v2.0...');

      // 添加本地存储相关字段
      db.exec(`
        ALTER TABLE users ADD COLUMN storage_permission TEXT DEFAULT 'sftp_only';
        ALTER TABLE users ADD COLUMN current_storage_type TEXT DEFAULT 'sftp';
        ALTER TABLE users ADD COLUMN local_storage_quota INTEGER DEFAULT ${DEFAULT_LOCAL_STORAGE_QUOTA_BYTES};
        ALTER TABLE users ADD COLUMN local_storage_used INTEGER DEFAULT 0;
        ALTER TABLE users ADD COLUMN oss_storage_quota INTEGER DEFAULT ${DEFAULT_OSS_STORAGE_QUOTA_BYTES};
      `);

      console.log('[数据库迁移] ✓ 用户表已升级');

      // 为分享表添加存储类型字段
      const shareColumns = db.prepare("PRAGMA table_info(shares)").all();
      const hasShareStorageType = shareColumns.some(col => col.name === 'storage_type');

      if (!hasShareStorageType) {
        db.exec(`ALTER TABLE shares ADD COLUMN storage_type TEXT DEFAULT 'sftp';`);
        console.log('[数据库迁移] ✓ 分享表已升级');
      }

      console.log('[数据库迁移] ✅ 数据库升级到 v2.0 完成！本地存储功能已启用');
    }
  } catch (error) {
    console.error('[数据库迁移] 迁移失败:', error);
    throw error;
  }
}

// 数据库版本迁移 - v3.0 SFTP → OSS
function migrateToOss() {
  try {
    const columns = db.prepare("PRAGMA table_info(users)").all();
    const hasOssProvider = columns.some(col => col.name === 'oss_provider');

    if (!hasOssProvider) {
      console.log('[数据库迁移] 检测到 SFTP 版本，开始升级到 v3.0 OSS...');

      // 添加 OSS 相关字段
      db.exec(`
        ALTER TABLE users ADD COLUMN oss_provider TEXT DEFAULT NULL;
        ALTER TABLE users ADD COLUMN oss_region TEXT DEFAULT NULL;
        ALTER TABLE users ADD COLUMN oss_access_key_id TEXT DEFAULT NULL;
        ALTER TABLE users ADD COLUMN oss_access_key_secret TEXT DEFAULT NULL;
        ALTER TABLE users ADD COLUMN oss_bucket TEXT DEFAULT NULL;
        ALTER TABLE users ADD COLUMN oss_endpoint TEXT DEFAULT NULL;
        ALTER TABLE users ADD COLUMN has_oss_config INTEGER DEFAULT 0;
      `);
      console.log('[数据库迁移] ✓ OSS 字段已添加');
    }

    // 修复：无论 OSS 字段是否刚添加，都要确保更新现有的 sftp 数据
    // 检查是否有用户仍使用 sftp 类型
    const sftpUsers = db.prepare("SELECT COUNT(*) as count FROM users WHERE storage_permission = 'sftp_only' OR current_storage_type = 'sftp'").get();
    if (sftpUsers.count > 0) {
      console.log(`[数据库迁移] 检测到 ${sftpUsers.count} 个用户仍使用 sftp 类型，正在更新...`);

      // 更新存储权限枚举值：sftp_only → oss_only
      db.exec(`UPDATE users SET storage_permission = 'oss_only' WHERE storage_permission = 'sftp_only'`);
      console.log('[数据库迁移] ✓ 存储权限枚举值已更新');

      // 更新存储类型：sftp → oss
      db.exec(`UPDATE users SET current_storage_type = 'oss' WHERE current_storage_type = 'sftp'`);
      console.log('[数据库迁移] ✓ 存储类型已更新');

      // 更新分享表的存储类型
      const shareColumns = db.prepare("PRAGMA table_info(shares)").all();
      const hasStorageType = shareColumns.some(col => col.name === 'storage_type');
      if (hasStorageType) {
        db.exec(`UPDATE shares SET storage_type = 'oss' WHERE storage_type = 'sftp'`);
        console.log('[数据库迁移] ✓ 分享表存储类型已更新');
      }

      console.log('[数据库迁移] ✅ SFTP → OSS 数据更新完成！');
    }
  } catch (error) {
    console.error('[数据库迁移] OSS 迁移失败:', error);
    // 不抛出错误，允许服务继续启动
  }
}


// 数据库迁移 - OSS 存储配额字段
function migrateOssQuotaField() {
  try {
    const columns = db.prepare("PRAGMA table_info(users)").all();
    const hasOssStorageQuota = columns.some(col => col.name === 'oss_storage_quota');

    if (!hasOssStorageQuota) {
      console.log('[数据库迁移] 添加 oss_storage_quota 字段...');
      db.exec(`ALTER TABLE users ADD COLUMN oss_storage_quota INTEGER DEFAULT ${DEFAULT_OSS_STORAGE_QUOTA_BYTES}`);
      console.log('[数据库迁移] ✓ oss_storage_quota 字段已添加');
    }

    // 统一策略：未配置或无效值默认 1GB
    const backfillResult = db.prepare(`
      UPDATE users
      SET oss_storage_quota = ?
      WHERE oss_storage_quota IS NULL OR oss_storage_quota <= 0
    `).run(DEFAULT_OSS_STORAGE_QUOTA_BYTES);

    if (backfillResult.changes > 0) {
      console.log(`[数据库迁移] ✓ OSS 配额默认值已回填: ${backfillResult.changes} 条记录`);
    }
  } catch (error) {
    console.error('[数据库迁移] oss_storage_quota 迁移失败:', error);
  }
}

// 数据库迁移 - 下载流量配额字段
function migrateDownloadTrafficFields() {
  try {
    const columns = db.prepare("PRAGMA table_info(users)").all();
    const hasDownloadTrafficQuota = columns.some(col => col.name === 'download_traffic_quota');
    const hasDownloadTrafficUsed = columns.some(col => col.name === 'download_traffic_used');
    const hasDownloadTrafficQuotaExpiresAt = columns.some(col => col.name === 'download_traffic_quota_expires_at');
    const hasDownloadTrafficResetCycle = columns.some(col => col.name === 'download_traffic_reset_cycle');
    const hasDownloadTrafficLastResetAt = columns.some(col => col.name === 'download_traffic_last_reset_at');

    if (!hasDownloadTrafficQuota) {
      console.log('[数据库迁移] 添加 download_traffic_quota 字段...');
      db.exec('ALTER TABLE users ADD COLUMN download_traffic_quota INTEGER DEFAULT 0');
      console.log('[数据库迁移] ✓ download_traffic_quota 字段已添加');
    }

    if (!hasDownloadTrafficUsed) {
      console.log('[数据库迁移] 添加 download_traffic_used 字段...');
      db.exec('ALTER TABLE users ADD COLUMN download_traffic_used INTEGER DEFAULT 0');
      console.log('[数据库迁移] ✓ download_traffic_used 字段已添加');
    }

    if (!hasDownloadTrafficQuotaExpiresAt) {
      console.log('[数据库迁移] 添加 download_traffic_quota_expires_at 字段...');
      db.exec('ALTER TABLE users ADD COLUMN download_traffic_quota_expires_at DATETIME DEFAULT NULL');
      console.log('[数据库迁移] ✓ download_traffic_quota_expires_at 字段已添加');
    }

    if (!hasDownloadTrafficResetCycle) {
      console.log('[数据库迁移] 添加 download_traffic_reset_cycle 字段...');
      db.exec("ALTER TABLE users ADD COLUMN download_traffic_reset_cycle TEXT DEFAULT 'none'");
      console.log('[数据库迁移] ✓ download_traffic_reset_cycle 字段已添加');
    }

    if (!hasDownloadTrafficLastResetAt) {
      console.log('[数据库迁移] 添加 download_traffic_last_reset_at 字段...');
      db.exec('ALTER TABLE users ADD COLUMN download_traffic_last_reset_at DATETIME DEFAULT NULL');
      console.log('[数据库迁移] ✓ download_traffic_last_reset_at 字段已添加');
    }

    // 统一策略：download_traffic_quota 为空时回填为 0（0 表示禁止下载，-1 表示不限流量）
    const quotaBackfillResult = db.prepare(`
      UPDATE users
      SET download_traffic_quota = 0
      WHERE download_traffic_quota IS NULL
    `).run();

    if (quotaBackfillResult.changes > 0) {
      console.log(`[数据库迁移] ✓ 下载流量配额默认值已回填: ${quotaBackfillResult.changes} 条记录`);
    }

    const quotaUnlimitedNormalizeResult = db.prepare(`
      UPDATE users
      SET download_traffic_quota = -1
      WHERE download_traffic_quota < 0
    `).run();

    if (quotaUnlimitedNormalizeResult.changes > 0) {
      console.log(`[数据库迁移] ✓ 不限流量配额已标准化为 -1: ${quotaUnlimitedNormalizeResult.changes} 条记录`);
    }

    const usedBackfillResult = db.prepare(`
      UPDATE users
      SET download_traffic_used = 0
      WHERE download_traffic_used IS NULL OR download_traffic_used < 0
    `).run();

    if (usedBackfillResult.changes > 0) {
      console.log(`[数据库迁移] ✓ 下载流量已用值已回填: ${usedBackfillResult.changes} 条记录`);
    }

    const usedCapResult = db.prepare(`
      UPDATE users
      SET download_traffic_used = download_traffic_quota
      WHERE download_traffic_quota >= 0 AND download_traffic_used > download_traffic_quota
    `).run();

    if (usedCapResult.changes > 0) {
      console.log(`[数据库迁移] ✓ 下载流量已用值已按配额校准: ${usedCapResult.changes} 条记录`);
    }

    const resetCycleBackfillResult = db.prepare(`
      UPDATE users
      SET download_traffic_reset_cycle = 'none'
      WHERE download_traffic_reset_cycle IS NULL
         OR download_traffic_reset_cycle NOT IN ('none', 'daily', 'weekly', 'monthly')
    `).run();

    if (resetCycleBackfillResult.changes > 0) {
      console.log(`[数据库迁移] ✓ 下载流量重置周期已回填: ${resetCycleBackfillResult.changes} 条记录`);
    }

    const clearExpiryForNonPositiveQuotaResult = db.prepare(`
      UPDATE users
      SET download_traffic_quota_expires_at = NULL
      WHERE download_traffic_quota <= 0
    `).run();

    if (clearExpiryForNonPositiveQuotaResult.changes > 0) {
      console.log(`[数据库迁移] ✓ 非正下载配额用户已清理到期时间: ${clearExpiryForNonPositiveQuotaResult.changes} 条记录`);
    }
  } catch (error) {
    console.error('[数据库迁移] 下载流量字段迁移失败:', error);
  }
}

// 下载流量报表（按天聚合）
const DownloadTrafficReportDB = {
  normalizeDays(days, defaultDays = 30, maxDays = 365) {
    const parsed = Number(days);
    if (!Number.isFinite(parsed) || parsed <= 0) {
      return defaultDays;
    }
    return Math.min(maxDays, Math.max(1, Math.floor(parsed)));
  },

  formatDateKey(date = new Date()) {
    const target = date instanceof Date ? date : new Date(date);
    if (Number.isNaN(target.getTime())) {
      return null;
    }
    const year = target.getFullYear();
    const month = String(target.getMonth() + 1).padStart(2, '0');
    const day = String(target.getDate()).padStart(2, '0');
    return `${year}-${month}-${day}`;
  },

  addUsage(userId, bytesUsed, downloadCount = 1, date = new Date()) {
    const uid = Number(userId);
    const bytes = Math.floor(Number(bytesUsed));
    const count = Math.floor(Number(downloadCount));
    const dateKey = this.formatDateKey(date);

    if (!Number.isFinite(uid) || uid <= 0 || !Number.isFinite(bytes) || bytes <= 0 || !dateKey) {
      return null;
    }

    const normalizedCount = Number.isFinite(count) && count > 0 ? count : 1;

    return db.prepare(`
      INSERT INTO user_download_traffic_daily (user_id, date_key, bytes_used, download_count, created_at, updated_at)
      VALUES (?, ?, ?, ?, datetime('now', 'localtime'), datetime('now', 'localtime'))
      ON CONFLICT(user_id, date_key)
      DO UPDATE SET
        bytes_used = user_download_traffic_daily.bytes_used + excluded.bytes_used,
        download_count = user_download_traffic_daily.download_count + excluded.download_count,
        updated_at = datetime('now', 'localtime')
    `).run(uid, dateKey, bytes, normalizedCount);
  },

  getDailyUsage(userId, days = 30) {
    const uid = Number(userId);
    if (!Number.isFinite(uid) || uid <= 0) {
      return [];
    }

    const safeDays = this.normalizeDays(days);
    const offset = `-${safeDays - 1} days`;

    return db.prepare(`
      SELECT
        date_key,
        bytes_used,
        download_count
      FROM user_download_traffic_daily
      WHERE user_id = ?
        AND date_key >= date('now', 'localtime', ?)
      ORDER BY date_key ASC
    `).all(uid, offset);
  },

  getPeriodSummary(userId, days = null) {
    const uid = Number(userId);
    if (!Number.isFinite(uid) || uid <= 0) {
      return { bytes_used: 0, download_count: 0 };
    }

    if (days === null || days === undefined) {
      const row = db.prepare(`
        SELECT
          COALESCE(SUM(bytes_used), 0) AS bytes_used,
          COALESCE(SUM(download_count), 0) AS download_count
        FROM user_download_traffic_daily
        WHERE user_id = ?
      `).get(uid);
      return row || { bytes_used: 0, download_count: 0 };
    }

    const safeDays = this.normalizeDays(days);
    const offset = `-${safeDays - 1} days`;

    const row = db.prepare(`
      SELECT
        COALESCE(SUM(bytes_used), 0) AS bytes_used,
        COALESCE(SUM(download_count), 0) AS download_count
      FROM user_download_traffic_daily
      WHERE user_id = ?
        AND date_key >= date('now', 'localtime', ?)
    `).get(uid, offset);

    return row || { bytes_used: 0, download_count: 0 };
  }
};

const DownloadTrafficReservationDB = {
  create({ userId, source = 'direct', objectKey = null, reservedBytes, expiresAt }) {
    const uid = Number(userId);
    const bytes = Math.floor(Number(reservedBytes));
    const expiresAtValue = typeof expiresAt === 'string' ? expiresAt : null;

    if (!Number.isFinite(uid) || uid <= 0 || !Number.isFinite(bytes) || bytes <= 0 || !expiresAtValue) {
      return null;
    }

    const result = db.prepare(`
      INSERT INTO user_download_traffic_reservations (
        user_id, source, object_key, reserved_bytes, remaining_bytes,
        status, expires_at, created_at, updated_at
      ) VALUES (?, ?, ?, ?, ?, 'pending', ?, datetime('now', 'localtime'), datetime('now', 'localtime'))
    `).run(uid, source, objectKey, bytes, bytes, expiresAtValue);

    return db.prepare('SELECT * FROM user_download_traffic_reservations WHERE id = ?').get(result.lastInsertRowid);
  },

  getPendingReservedBytes(userId) {
    const uid = Number(userId);
    if (!Number.isFinite(uid) || uid <= 0) {
      return 0;
    }

    const row = db.prepare(`
      SELECT COALESCE(SUM(remaining_bytes), 0) AS reserved
      FROM user_download_traffic_reservations
      WHERE user_id = ?
        AND status = 'pending'
        AND expires_at > datetime('now', 'localtime')
    `).get(uid);

    return Number(row?.reserved || 0);
  },

  expirePendingReservations() {
    return db.prepare(`
      UPDATE user_download_traffic_reservations
      SET status = 'expired',
          finalized_at = datetime('now', 'localtime'),
          updated_at = datetime('now', 'localtime')
      WHERE status = 'pending'
        AND expires_at <= datetime('now', 'localtime')
    `).run();
  },

  consumePendingBytes(userId, bytes) {
    const uid = Number(userId);
    let remaining = Math.floor(Number(bytes));
    if (!Number.isFinite(uid) || uid <= 0 || !Number.isFinite(remaining) || remaining <= 0) {
      return { consumed: 0, finalizedCount: 0 };
    }

    const pendingRows = db.prepare(`
      SELECT id, remaining_bytes, status
      FROM user_download_traffic_reservations
      WHERE user_id = ?
        AND status IN ('pending', 'expired')
        AND remaining_bytes > 0
      ORDER BY
        CASE WHEN status = 'pending' THEN 0 ELSE 1 END,
        created_at ASC,
        id ASC
    `).all(uid);

    let consumed = 0;
    let finalizedCount = 0;

    for (const row of pendingRows) {
      if (remaining <= 0) break;

      const rowRemaining = Number(row.remaining_bytes || 0);
      if (!Number.isFinite(rowRemaining) || rowRemaining <= 0) continue;

      const useBytes = Math.min(remaining, rowRemaining);
      const nextRemaining = rowRemaining - useBytes;
      const nextStatus = nextRemaining <= 0 ? 'confirmed' : row.status;

      db.prepare(`
        UPDATE user_download_traffic_reservations
        SET remaining_bytes = ?,
            status = ?,
            finalized_at = CASE WHEN ? = 'confirmed' THEN datetime('now', 'localtime') ELSE finalized_at END,
            updated_at = datetime('now', 'localtime')
        WHERE id = ?
      `).run(nextRemaining, nextStatus, nextStatus, row.id);

      consumed += useBytes;
      remaining -= useBytes;
      if (nextStatus === 'confirmed') {
        finalizedCount += 1;
      }
    }

    return { consumed, finalizedCount };
  },

  getAdminList(options = {}) {
    const where = ['1=1'];
    const params = [];

    const status = typeof options.status === 'string' ? options.status.trim() : '';
    const allowedStatus = new Set(['pending', 'confirmed', 'expired', 'cancelled']);
    if (allowedStatus.has(status)) {
      where.push('r.status = ?');
      params.push(status);
    }

    const uid = Number(options.userId);
    if (Number.isFinite(uid) && uid > 0) {
      where.push('r.user_id = ?');
      params.push(Math.floor(uid));
    }

    const keyword = typeof options.keyword === 'string' ? options.keyword.trim() : '';
    if (keyword) {
      const likeValue = `%${keyword}%`;
      where.push('(u.username LIKE ? OR COALESCE(r.object_key, \'\') LIKE ? OR r.source LIKE ?)');
      params.push(likeValue, likeValue, likeValue);
    }

    const whereSql = where.join(' AND ');
    const page = Math.max(1, Math.floor(Number(options.page) || 1));
    const pageSize = Math.min(200, Math.max(1, Math.floor(Number(options.pageSize) || 20)));
    const offset = (page - 1) * pageSize;

    const totalRow = db.prepare(`
      SELECT COUNT(*) AS total
      FROM user_download_traffic_reservations r
      LEFT JOIN users u ON u.id = r.user_id
      WHERE ${whereSql}
    `).get(...params);
    const total = Math.max(0, Number(totalRow?.total || 0));
    const totalPages = Math.max(1, Math.ceil(total / pageSize));
    const safePage = Math.min(page, totalPages);
    const safeOffset = (safePage - 1) * pageSize;

    const rows = db.prepare(`
      SELECT
        r.*,
        u.username
      FROM user_download_traffic_reservations r
      LEFT JOIN users u ON u.id = r.user_id
      WHERE ${whereSql}
      ORDER BY r.created_at DESC, r.id DESC
      LIMIT ? OFFSET ?
    `).all(...params, pageSize, safeOffset);

    return {
      rows,
      pagination: {
        page: safePage,
        pageSize,
        total,
        totalPages
      }
    };
  },

  getAdminSummary() {
    const statusRows = db.prepare(`
      SELECT
        status,
        COUNT(*) AS count,
        COALESCE(SUM(remaining_bytes), 0) AS remaining_bytes,
        COALESCE(SUM(reserved_bytes), 0) AS reserved_bytes
      FROM user_download_traffic_reservations
      GROUP BY status
    `).all();

    const summary = {
      total: 0,
      pending: 0,
      confirmed: 0,
      expired: 0,
      cancelled: 0,
      pending_remaining_bytes: 0,
      pending_reserved_bytes: 0,
      pending_expiring_soon: 0
    };

    for (const row of statusRows) {
      const status = String(row.status || '').trim();
      const count = Number(row.count || 0);
      const remaining = Number(row.remaining_bytes || 0);
      const reserved = Number(row.reserved_bytes || 0);

      summary.total += count;
      if (Object.prototype.hasOwnProperty.call(summary, status)) {
        summary[status] = count;
      }
      if (status === 'pending') {
        summary.pending_remaining_bytes = Math.max(0, remaining);
        summary.pending_reserved_bytes = Math.max(0, reserved);
      }
    }

    const expiringSoonRow = db.prepare(`
      SELECT COUNT(*) AS count
      FROM user_download_traffic_reservations
      WHERE status = 'pending'
        AND expires_at <= datetime('now', 'localtime', '+5 minutes')
    `).get();
    summary.pending_expiring_soon = Number(expiringSoonRow?.count || 0);

    return summary;
  },

  cancelPendingById(id) {
    const rid = Number(id);
    if (!Number.isFinite(rid) || rid <= 0) {
      return { changes: 0, row: null };
    }

    const row = db.prepare(`
      SELECT *
      FROM user_download_traffic_reservations
      WHERE id = ?
        AND status = 'pending'
    `).get(Math.floor(rid));
    if (!row) {
      return { changes: 0, row: null };
    }

    const result = db.prepare(`
      UPDATE user_download_traffic_reservations
      SET status = 'cancelled',
          remaining_bytes = 0,
          finalized_at = datetime('now', 'localtime'),
          updated_at = datetime('now', 'localtime')
      WHERE id = ?
        AND status = 'pending'
    `).run(Math.floor(rid));

    return {
      changes: Number(result?.changes || 0),
      row: db.prepare('SELECT * FROM user_download_traffic_reservations WHERE id = ?').get(Math.floor(rid))
    };
  },

  cleanupFinalizedHistory(keepDays = 0) {
    // keepDays=0 表示立即清理全部已完成历史（confirmed/expired/cancelled）
    const normalized = Number(keepDays);
    const days = Number.isFinite(normalized)
      ? Math.min(365, Math.max(0, Math.floor(normalized)))
      : 0;
    return db.prepare(`
      DELETE FROM user_download_traffic_reservations
      WHERE (
        status IN ('confirmed', 'cancelled')
        OR (status = 'expired' AND COALESCE(remaining_bytes, 0) <= 0)
      )
        AND updated_at < datetime('now', 'localtime', '-' || ? || ' days')
    `).run(days);
  }
};

const UploadSessionDB = {
  _normalizeSessionId(sessionId) {
    return typeof sessionId === 'string' ? sessionId.trim() : '';
  },

  parseUploadedChunks(chunksText) {
    const raw = typeof chunksText === 'string' ? chunksText.trim() : '';
    if (!raw) return [];
    const set = new Set();
    for (const part of raw.split(',')) {
      const idx = Number(part);
      if (Number.isFinite(idx) && idx >= 0) {
        set.add(Math.floor(idx));
      }
    }
    return Array.from(set).sort((a, b) => a - b);
  },

  formatUploadedChunks(chunks = []) {
    if (!Array.isArray(chunks) || chunks.length === 0) {
      return '';
    }
    const set = new Set();
    for (const chunk of chunks) {
      const idx = Number(chunk);
      if (Number.isFinite(idx) && idx >= 0) {
        set.add(Math.floor(idx));
      }
    }
    return Array.from(set).sort((a, b) => a - b).join(',');
  },

  create(data = {}) {
    const sessionId = this._normalizeSessionId(data.sessionId);
    const userId = Number(data.userId);
    const storageType = typeof data.storageType === 'string' ? data.storageType : 'local';
    const targetPath = typeof data.targetPath === 'string' ? data.targetPath : '';
    const fileName = typeof data.fileName === 'string' ? data.fileName : '';
    const fileSize = Math.max(0, Math.floor(Number(data.fileSize) || 0));
    const chunkSize = Math.max(1, Math.floor(Number(data.chunkSize) || 1));
    const totalChunks = Math.max(1, Math.floor(Number(data.totalChunks) || 1));
    const uploadedChunks = this.formatUploadedChunks(data.uploadedChunks || []);
    const uploadedBytes = Math.max(0, Math.floor(Number(data.uploadedBytes) || 0));
    const tempFilePath = typeof data.tempFilePath === 'string' ? data.tempFilePath : '';
    const fileHash = typeof data.fileHash === 'string' ? data.fileHash : null;
    const expiresAt = typeof data.expiresAt === 'string' ? data.expiresAt : null;

    if (!sessionId || !Number.isFinite(userId) || userId <= 0 || !targetPath || !fileName || !tempFilePath || !expiresAt) {
      return null;
    }

    db.prepare(`
      INSERT INTO upload_sessions (
        session_id, user_id, storage_type, target_path, file_name,
        file_size, chunk_size, total_chunks, uploaded_chunks, uploaded_bytes,
        temp_file_path, file_hash, status, expires_at, created_at, updated_at
      ) VALUES (
        ?, ?, ?, ?, ?,
        ?, ?, ?, ?, ?,
        ?, ?, 'active', ?, datetime('now', 'localtime'), datetime('now', 'localtime')
      )
      ON CONFLICT(session_id) DO UPDATE SET
        storage_type = excluded.storage_type,
        target_path = excluded.target_path,
        file_name = excluded.file_name,
        file_size = excluded.file_size,
        chunk_size = excluded.chunk_size,
        total_chunks = excluded.total_chunks,
        uploaded_chunks = excluded.uploaded_chunks,
        uploaded_bytes = excluded.uploaded_bytes,
        temp_file_path = excluded.temp_file_path,
        file_hash = excluded.file_hash,
        status = 'active',
        expires_at = excluded.expires_at,
        completed_at = NULL,
        updated_at = datetime('now', 'localtime')
    `).run(
      sessionId,
      Math.floor(userId),
      storageType,
      targetPath,
      fileName,
      fileSize,
      chunkSize,
      totalChunks,
      uploadedChunks,
      uploadedBytes,
      tempFilePath,
      fileHash,
      expiresAt
    );

    return this.findBySessionId(sessionId);
  },

  findBySessionId(sessionId) {
    const sid = this._normalizeSessionId(sessionId);
    if (!sid) return null;
    return db.prepare('SELECT * FROM upload_sessions WHERE session_id = ?').get(sid);
  },

  findActiveForResume(userId, storageType, targetPath, fileSize, fileHash = null) {
    const uid = Number(userId);
    const safeStorageType = storageType === 'local' ? 'local' : 'oss';
    const safePath = typeof targetPath === 'string' ? targetPath : '';
    const size = Math.floor(Number(fileSize) || 0);
    const hash = typeof fileHash === 'string' ? fileHash.trim() : '';
    if (!Number.isFinite(uid) || uid <= 0 || !safePath || size <= 0) {
      return null;
    }

    if (hash) {
      return db.prepare(`
        SELECT *
        FROM upload_sessions
        WHERE user_id = ?
          AND storage_type = ?
          AND target_path = ?
          AND file_size = ?
          AND status = 'active'
          AND expires_at > datetime('now', 'localtime')
          AND COALESCE(file_hash, '') = ?
        ORDER BY updated_at DESC, created_at DESC
        LIMIT 1
      `).get(Math.floor(uid), safeStorageType, safePath, size, hash);
    }

    return db.prepare(`
      SELECT *
      FROM upload_sessions
      WHERE user_id = ?
        AND storage_type = ?
        AND target_path = ?
        AND file_size = ?
        AND status = 'active'
        AND expires_at > datetime('now', 'localtime')
      ORDER BY updated_at DESC, created_at DESC
      LIMIT 1
    `).get(Math.floor(uid), safeStorageType, safePath, size);
  },

  updateProgress(sessionId, uploadedChunks = [], uploadedBytes = 0, expiresAt = null) {
    const sid = this._normalizeSessionId(sessionId);
    if (!sid) return { changes: 0 };
    const chunksText = this.formatUploadedChunks(uploadedChunks);
    const bytes = Math.max(0, Math.floor(Number(uploadedBytes) || 0));

    if (typeof expiresAt === 'string' && expiresAt) {
      return db.prepare(`
        UPDATE upload_sessions
        SET uploaded_chunks = ?,
            uploaded_bytes = ?,
            expires_at = ?,
            updated_at = datetime('now', 'localtime')
        WHERE session_id = ?
      `).run(chunksText, bytes, expiresAt, sid);
    }

    return db.prepare(`
      UPDATE upload_sessions
      SET uploaded_chunks = ?,
          uploaded_bytes = ?,
          updated_at = datetime('now', 'localtime')
      WHERE session_id = ?
    `).run(chunksText, bytes, sid);
  },

  setStatus(sessionId, status, { completed = false, expiresAt = null } = {}) {
    const sid = this._normalizeSessionId(sessionId);
    const safeStatus = typeof status === 'string' ? status : '';
    if (!sid || !safeStatus) return { changes: 0 };

    if (expiresAt && completed) {
      return db.prepare(`
        UPDATE upload_sessions
        SET status = ?,
            expires_at = ?,
            completed_at = datetime('now', 'localtime'),
            updated_at = datetime('now', 'localtime')
        WHERE session_id = ?
      `).run(safeStatus, expiresAt, sid);
    }

    if (expiresAt) {
      return db.prepare(`
        UPDATE upload_sessions
        SET status = ?,
            expires_at = ?,
            completed_at = CASE WHEN ? = 1 THEN datetime('now', 'localtime') ELSE completed_at END,
            updated_at = datetime('now', 'localtime')
        WHERE session_id = ?
      `).run(safeStatus, expiresAt, completed ? 1 : 0, sid);
    }

    return db.prepare(`
      UPDATE upload_sessions
      SET status = ?,
          completed_at = CASE WHEN ? = 1 THEN datetime('now', 'localtime') ELSE completed_at END,
          updated_at = datetime('now', 'localtime')
      WHERE session_id = ?
    `).run(safeStatus, completed ? 1 : 0, sid);
  },

  delete(sessionId) {
    const sid = this._normalizeSessionId(sessionId);
    if (!sid) return { changes: 0 };
    return db.prepare('DELETE FROM upload_sessions WHERE session_id = ?').run(sid);
  },

  listExpiredActive(limit = 100) {
    const safeLimit = Math.min(500, Math.max(1, Math.floor(Number(limit) || 100)));
    return db.prepare(`
      SELECT *
      FROM upload_sessions
      WHERE status = 'active'
        AND expires_at <= datetime('now', 'localtime')
      ORDER BY expires_at ASC
      LIMIT ?
    `).all(safeLimit);
  },

  expireSession(sessionId) {
    const sid = this._normalizeSessionId(sessionId);
    if (!sid) return { changes: 0 };
    return db.prepare(`
      UPDATE upload_sessions
      SET status = 'expired',
          updated_at = datetime('now', 'localtime')
      WHERE session_id = ?
        AND status = 'active'
    `).run(sid);
  }
};

const DeviceSessionDB = {
  _normalizeSessionId(sessionId) {
    return typeof sessionId === 'string' ? sessionId.trim() : '';
  },

  _normalizeText(value, maxLength = 255) {
    if (typeof value !== 'string') return null;
    const trimmed = value.trim();
    if (!trimmed) return null;
    return trimmed.slice(0, maxLength);
  },

  _normalizeClientType(value) {
    const normalized = String(value || '').trim().toLowerCase();
    if (['web', 'desktop', 'mobile', 'api'].includes(normalized)) {
      return normalized;
    }
    return 'web';
  },

  create(data = {}) {
    const sessionId = this._normalizeSessionId(data.sessionId);
    const userId = Number(data.userId);
    const clientType = this._normalizeClientType(data.clientType);
    const deviceId = this._normalizeText(data.deviceId, 128);
    const deviceName = this._normalizeText(data.deviceName, 120);
    const platform = this._normalizeText(data.platform, 80);
    const ipAddress = this._normalizeText(data.ipAddress, 80);
    const userAgent = this._normalizeText(data.userAgent, 1024);
    const ttlDays = Math.max(1, Math.min(30, Number(data.ttlDays || 7)));

    if (!sessionId || !Number.isFinite(userId) || userId <= 0) {
      return null;
    }

    db.prepare(`
      INSERT INTO user_device_sessions (
        session_id, user_id, client_type, device_id, device_name,
        platform, ip_address, user_agent, created_at, last_active_at,
        expires_at, revoked_at, revoked_reason, updated_at
      ) VALUES (
        ?, ?, ?, ?, ?,
        ?, ?, ?, datetime('now', 'localtime'), datetime('now', 'localtime'),
        datetime('now', 'localtime', '+' || ? || ' days'), NULL, NULL, datetime('now', 'localtime')
      )
      ON CONFLICT(session_id) DO UPDATE SET
        user_id = excluded.user_id,
        client_type = excluded.client_type,
        device_id = excluded.device_id,
        device_name = excluded.device_name,
        platform = excluded.platform,
        ip_address = excluded.ip_address,
        user_agent = excluded.user_agent,
        last_active_at = datetime('now', 'localtime'),
        expires_at = datetime('now', 'localtime', '+' || ? || ' days'),
        revoked_at = NULL,
        revoked_reason = NULL,
        updated_at = datetime('now', 'localtime')
    `).run(
      sessionId,
      Math.floor(userId),
      clientType,
      deviceId,
      deviceName,
      platform,
      ipAddress,
      userAgent,
      ttlDays,
      ttlDays
    );

    return this.findBySessionId(sessionId);
  },

  findBySessionId(sessionId) {
    const sid = this._normalizeSessionId(sessionId);
    if (!sid) return null;
    return db.prepare('SELECT * FROM user_device_sessions WHERE session_id = ?').get(sid);
  },

  findActiveBySessionId(sessionId) {
    const sid = this._normalizeSessionId(sessionId);
    if (!sid) return null;
    return db.prepare(`
      SELECT *
      FROM user_device_sessions
      WHERE session_id = ?
        AND revoked_at IS NULL
        AND expires_at > datetime('now', 'localtime')
      LIMIT 1
    `).get(sid);
  },

  listActiveByUser(userId, limit = 60) {
    const uid = Number(userId);
    if (!Number.isFinite(uid) || uid <= 0) return [];
    const safeLimit = Math.max(1, Math.min(200, Math.floor(Number(limit) || 60)));
    return db.prepare(`
      SELECT *
      FROM user_device_sessions
      WHERE user_id = ?
        AND revoked_at IS NULL
        AND expires_at > datetime('now', 'localtime')
      ORDER BY datetime(COALESCE(last_active_at, created_at)) DESC, created_at DESC
      LIMIT ?
    `).all(Math.floor(uid), safeLimit);
  },

  listActiveByDevice(userId, options = {}) {
    const uid = Number(userId);
    if (!Number.isFinite(uid) || uid <= 0) return [];

    const clientType = this._normalizeClientType(options.clientType);
    const deviceId = this._normalizeText(options.deviceId, 128);
    const deviceName = this._normalizeText(options.deviceName, 120);
    const platform = this._normalizeText(options.platform, 80);
    const safeLimit = Math.max(1, Math.min(50, Math.floor(Number(options.limit) || 20)));

    if (deviceId) {
      return db.prepare(`
        SELECT *
        FROM user_device_sessions
        WHERE user_id = ?
          AND client_type = ?
          AND device_id = ?
          AND revoked_at IS NULL
          AND expires_at > datetime('now', 'localtime')
        ORDER BY datetime(COALESCE(last_active_at, created_at)) DESC, created_at DESC
        LIMIT ?
      `).all(Math.floor(uid), clientType, deviceId, safeLimit);
    }

    if (clientType !== 'desktop') {
      return [];
    }

    const extraConditions = [];
    const params = [Math.floor(uid), clientType];
    if (deviceName) {
      extraConditions.push("COALESCE(device_name, '') = ?");
      params.push(deviceName);
    }
    if (platform) {
      extraConditions.push("COALESCE(platform, '') = ?");
      params.push(platform);
    }
    if (extraConditions.length === 0) {
      return [];
    }

    params.push(safeLimit);
    return db.prepare(`
      SELECT *
      FROM user_device_sessions
      WHERE user_id = ?
        AND client_type = ?
        AND ${extraConditions.join(' AND ')}
        AND revoked_at IS NULL
        AND expires_at > datetime('now', 'localtime')
      ORDER BY datetime(COALESCE(last_active_at, created_at)) DESC, created_at DESC
      LIMIT ?
    `).all(...params);
  },

  touch(sessionId, options = {}) {
    const sid = this._normalizeSessionId(sessionId);
    if (!sid) return { changes: 0 };
    const ipAddress = this._normalizeText(options.ipAddress, 80);
    const userAgent = this._normalizeText(options.userAgent, 1024);

    return db.prepare(`
      UPDATE user_device_sessions
      SET ip_address = COALESCE(?, ip_address),
          user_agent = COALESCE(?, user_agent),
          last_active_at = datetime('now', 'localtime'),
          updated_at = datetime('now', 'localtime')
      WHERE session_id = ?
        AND revoked_at IS NULL
        AND expires_at > datetime('now', 'localtime')
    `).run(ipAddress, userAgent, sid);
  },

  revoke(sessionId, userId = null, reason = 'manual') {
    const sid = this._normalizeSessionId(sessionId);
    if (!sid) return { changes: 0 };
    const revokeReason = this._normalizeText(reason, 120) || 'manual';
    if (userId !== null && userId !== undefined) {
      const uid = Number(userId);
      if (!Number.isFinite(uid) || uid <= 0) return { changes: 0 };
      return db.prepare(`
        UPDATE user_device_sessions
        SET revoked_at = datetime('now', 'localtime'),
            revoked_reason = ?,
            updated_at = datetime('now', 'localtime')
        WHERE session_id = ?
          AND user_id = ?
          AND revoked_at IS NULL
      `).run(revokeReason, sid, Math.floor(uid));
    }
    return db.prepare(`
      UPDATE user_device_sessions
      SET revoked_at = datetime('now', 'localtime'),
          revoked_reason = ?,
          updated_at = datetime('now', 'localtime')
      WHERE session_id = ?
        AND revoked_at IS NULL
    `).run(revokeReason, sid);
  },

  cleanupExpired(keepDays = 30) {
    const safeDays = Math.max(1, Math.min(365, Math.floor(Number(keepDays) || 30)));
    return db.prepare(`
      DELETE FROM user_device_sessions
      WHERE (expires_at <= datetime('now', 'localtime', '-' || ? || ' days'))
         OR (revoked_at IS NOT NULL AND revoked_at <= datetime('now', 'localtime', '-' || ? || ' days'))
    `).run(safeDays, safeDays);
  }
};

const FileHashIndexDB = {
  normalizeStorageType(storageType) {
    return storageType === 'oss' ? 'oss' : 'local';
  },

  upsert(entry = {}) {
    const userId = Number(entry.userId);
    const storageType = this.normalizeStorageType(entry.storageType);
    const fileHash = typeof entry.fileHash === 'string' ? entry.fileHash.trim() : '';
    const fileSize = Math.max(0, Math.floor(Number(entry.fileSize) || 0));
    const filePath = typeof entry.filePath === 'string' ? entry.filePath : '';
    const objectKey = typeof entry.objectKey === 'string' && entry.objectKey ? entry.objectKey : null;

    if (!Number.isFinite(userId) || userId <= 0 || !fileHash || fileSize <= 0 || !filePath) {
      return null;
    }

    return db.prepare(`
      INSERT INTO user_file_hash_index (
        user_id, storage_type, file_hash, file_size, file_path, object_key, created_at, updated_at
      ) VALUES (?, ?, ?, ?, ?, ?, datetime('now', 'localtime'), datetime('now', 'localtime'))
      ON CONFLICT(user_id, storage_type, file_hash, file_size, file_path)
      DO UPDATE SET
        object_key = excluded.object_key,
        updated_at = datetime('now', 'localtime')
    `).run(
      Math.floor(userId),
      storageType,
      fileHash,
      fileSize,
      filePath,
      objectKey
    );
  },

  findLatestByHash(userId, storageType, fileHash, fileSize) {
    const uid = Number(userId);
    const normalizedStorage = this.normalizeStorageType(storageType);
    const hash = typeof fileHash === 'string' ? fileHash.trim() : '';
    const size = Math.max(0, Math.floor(Number(fileSize) || 0));
    if (!Number.isFinite(uid) || uid <= 0 || !hash || size <= 0) {
      return null;
    }

    return db.prepare(`
      SELECT *
      FROM user_file_hash_index
      WHERE user_id = ?
        AND storage_type = ?
        AND file_hash = ?
        AND file_size = ?
      ORDER BY updated_at DESC, id DESC
      LIMIT 1
    `).get(Math.floor(uid), normalizedStorage, hash, size);
  },

  deleteByPath(userId, storageType, filePath) {
    const uid = Number(userId);
    const normalizedStorage = this.normalizeStorageType(storageType);
    const safePath = typeof filePath === 'string' ? filePath : '';
    if (!Number.isFinite(uid) || uid <= 0 || !safePath) {
      return { changes: 0 };
    }

    return db.prepare(`
      DELETE FROM user_file_hash_index
      WHERE user_id = ?
        AND storage_type = ?
        AND file_path = ?
    `).run(Math.floor(uid), normalizedStorage, safePath);
  },

  getByPath(userId, storageType, filePath) {
    const uid = Number(userId);
    const normalizedStorage = this.normalizeStorageType(storageType);
    const safePath = typeof filePath === 'string' ? filePath : '';
    if (!Number.isFinite(uid) || uid <= 0 || !safePath) {
      return null;
    }

    return db.prepare(`
      SELECT *
      FROM user_file_hash_index
      WHERE user_id = ?
        AND storage_type = ?
        AND file_path = ?
      ORDER BY updated_at DESC, id DESC
      LIMIT 1
    `).get(Math.floor(uid), normalizedStorage, safePath);
  }
};

const DownloadTrafficIngestDB = {
  isProcessed(bucket, logKey, etag = null) {
    const row = db.prepare(`
      SELECT id
      FROM download_traffic_ingested_logs
      WHERE bucket = ?
        AND log_key = ?
        AND COALESCE(etag, '') = COALESCE(?, '')
      LIMIT 1
    `).get(bucket, logKey, etag);

    return !!row;
  },

  markProcessed({
    bucket,
    logKey,
    etag = null,
    fileSize = 0,
    lineCount = 0,
    bytesCount = 0,
    status = 'success',
    errorMessage = null
  }) {
    return db.prepare(`
      INSERT INTO download_traffic_ingested_logs (
        bucket, log_key, etag, file_size, line_count, bytes_count,
        status, error_message, processed_at
      ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, datetime('now', 'localtime'))
      ON CONFLICT(bucket, log_key, etag)
      DO UPDATE SET
        file_size = excluded.file_size,
        line_count = excluded.line_count,
        bytes_count = excluded.bytes_count,
        status = excluded.status,
        error_message = excluded.error_message,
        processed_at = datetime('now', 'localtime')
    `).run(
      bucket,
      logKey,
      etag || '',
      Number(fileSize) || 0,
      Number(lineCount) || 0,
      Number(bytesCount) || 0,
      status || 'success',
      errorMessage || null
    );
  }
};

// 系统日志操作
const SystemLogDB = {
  // 日志级别常量
  LEVELS: {
    DEBUG: 'debug',
    INFO: 'info',
    WARN: 'warn',
    ERROR: 'error'
  },

  // 日志分类常量
  CATEGORIES: {
    AUTH: 'auth',           // 认证相关（登录、登出、注册）
    USER: 'user',           // 用户管理（创建、修改、删除、封禁）
    FILE: 'file',           // 文件操作（上传、下载、删除、重命名）
    SHARE: 'share',         // 分享操作（创建、删除、访问）
    SYSTEM: 'system',       // 系统操作（设置修改、服务启动）
    SECURITY: 'security'    // 安全事件（登录失败、暴力破解、异常访问）
  },

  // 写入日志
  log({ level = 'info', category, action, message, userId = null, username = null, ipAddress = null, userAgent = null, details = null }) {
    try {
      const stmt = db.prepare(`
        INSERT INTO system_logs (level, category, action, message, user_id, username, ip_address, user_agent, details, created_at)
        VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now', 'localtime'))
      `);

      const detailsStr = details ? (typeof details === 'string' ? details : JSON.stringify(details)) : null;

      stmt.run(level, category, action, message, userId, username, ipAddress, userAgent, detailsStr);
    } catch (error) {
      console.error('写入日志失败:', error);
    }
  },

  // 查询日志（支持分页和筛选）
  query({ page = 1, pageSize = 50, level = null, category = null, userId = null, startDate = null, endDate = null, keyword = null }) {
    let sql = 'SELECT * FROM system_logs WHERE 1=1';
    let countSql = 'SELECT COUNT(*) as total FROM system_logs WHERE 1=1';
    const params = [];

    if (level) {
      sql += ' AND level = ?';
      countSql += ' AND level = ?';
      params.push(level);
    }

    if (category) {
      sql += ' AND category = ?';
      countSql += ' AND category = ?';
      params.push(category);
    }

    if (userId) {
      sql += ' AND user_id = ?';
      countSql += ' AND user_id = ?';
      params.push(userId);
    }

    if (startDate) {
      sql += ' AND created_at >= ?';
      countSql += ' AND created_at >= ?';
      params.push(startDate);
    }

    if (endDate) {
      sql += ' AND created_at <= ?';
      countSql += ' AND created_at <= ?';
      params.push(endDate);
    }

    if (keyword) {
      sql += ' AND (message LIKE ? OR username LIKE ? OR action LIKE ?)';
      countSql += ' AND (message LIKE ? OR username LIKE ? OR action LIKE ?)';
      const kw = `%${keyword}%`;
      params.push(kw, kw, kw);
    }

    // 获取总数
    const totalResult = db.prepare(countSql).get(...params);
    const total = totalResult ? totalResult.total : 0;

    // 分页查询
    sql += ' ORDER BY created_at DESC LIMIT ? OFFSET ?';
    const offset = (page - 1) * pageSize;

    const logs = db.prepare(sql).all(...params, pageSize, offset);

    return {
      logs,
      total,
      page,
      pageSize,
      totalPages: Math.ceil(total / pageSize)
    };
  },

  // 获取最近的日志
  getRecent(limit = 100) {
    return db.prepare('SELECT * FROM system_logs ORDER BY created_at DESC LIMIT ?').all(limit);
  },

  // 按分类统计
  getStatsByCategory() {
    return db.prepare(`
      SELECT category, COUNT(*) as count
      FROM system_logs
      GROUP BY category
      ORDER BY count DESC
    `).all();
  },

  // 按日期统计（最近7天）
  getStatsByDate(days = 7) {
    return db.prepare(`
      SELECT DATE(created_at) as date, COUNT(*) as count
      FROM system_logs
      WHERE created_at >= datetime('now', 'localtime', '-' || ? || ' days')
      GROUP BY DATE(created_at)
      ORDER BY date DESC
    `).all(days);
  },

  // 清理旧日志（保留指定天数）
  cleanup(keepDays = 90) {
    const result = db.prepare(`
      DELETE FROM system_logs
      WHERE created_at < datetime('now', 'localtime', '-' || ? || ' days')
    `).run(keepDays);

    return result.changes;
  }
};

// 事务工具函数
const TransactionDB = {
  /**
   * 在事务中执行操作
   * @param {Function} fn - 要执行的函数，接收 db 作为参数
   * @returns {*} 函数返回值
   * @throws {Error} 如果事务失败则抛出错误
   */
  run(fn) {
    const transaction = db.transaction((callback) => {
      return callback(db);
    });
    return transaction(fn);
  },

  /**
   * 删除用户及其所有相关数据（使用事务）
   * @param {number} userId - 用户ID
   * @returns {object} 删除结果
   */
  deleteUserWithData(userId) {
    return this.run(() => {
      // 1. 删除用户的所有分享
      const sharesDeleted = db.prepare('DELETE FROM shares WHERE user_id = ?').run(userId);

      // 2. 删除用户的所有直链
      const directLinksDeleted = db.prepare('DELETE FROM direct_links WHERE user_id = ?').run(userId);

      // 3. 删除密码重置令牌
      const tokensDeleted = db.prepare('DELETE FROM password_reset_tokens WHERE user_id = ?').run(userId);

      // 4. 更新日志中的用户引用（设为 NULL，保留日志记录）
      db.prepare('UPDATE system_logs SET user_id = NULL WHERE user_id = ?').run(userId);

      // 5. 删除用户记录
      const userDeleted = db.prepare('DELETE FROM users WHERE id = ?').run(userId);

      return {
        sharesDeleted: sharesDeleted.changes,
        directLinksDeleted: directLinksDeleted.changes,
        tokensDeleted: tokensDeleted.changes,
        userDeleted: userDeleted.changes
      };
    });
  }
};

// 初始化数据库
initDatabase();
createDefaultAdmin();
initDefaultSettings();
migrateToV2(); // 执行数据库迁移
migrateThemePreference(); // 主题偏好迁移
migrateToOss(); // SFTP → OSS 迁移
migrateOssQuotaField(); // OSS 配额字段迁移
migrateDownloadTrafficFields(); // 下载流量字段迁移

module.exports = {
  db,
  UserDB,
  ShareDB,
  DirectLinkDB,
  SettingsDB,
  VerificationDB,
  PasswordResetTokenDB,
  DownloadTrafficReportDB,
  DownloadTrafficReservationDB,
  UploadSessionDB,
  DeviceSessionDB,
  FileHashIndexDB,
  DownloadTrafficIngestDB,
  SystemLogDB,
  TransactionDB,
  WalManager
};
