import crypto from 'node:crypto';
import fs from 'node:fs/promises';
import path from 'node:path';

const WORKSPACE_TEMP_SKIP = new Set([
  '.tkmindhints',
  '.goosehints',
  '.tkmind-profile.json',
  '.agents',
  '.goose',
]);

function candidateId(kind, key) {
  return `${kind}:${crypto.createHash('sha256').update(key).digest('hex').slice(0, 24)}`;
}

async function fileSize(targetPath) {
  try {
    const stat = await fs.stat(targetPath);
    return stat.isFile() ? stat.size : 0;
  } catch {
    return 0;
  }
}

async function walkFiles(rootDir, onFile) {
  let entries;
  try {
    entries = await fs.readdir(rootDir, { withFileTypes: true });
  } catch {
    return;
  }
  for (const entry of entries) {
    const fullPath = path.join(rootDir, entry.name);
    if (entry.isDirectory()) {
      if (WORKSPACE_TEMP_SKIP.has(entry.name)) continue;
      await walkFiles(fullPath, onFile);
      continue;
    }
    if (!entry.isFile()) continue;
    await onFile(fullPath);
  }
}

export function createCleanupService(pool, options = {}) {
  const storageRoot = path.resolve(options.storageRoot ?? path.join(process.cwd(), 'data', 'mindspace'));
  const h5Root = path.resolve(options.h5Root ?? process.cwd());

  const absoluteStoragePath = (storageKey) => {
    const resolved = path.resolve(storageRoot, storageKey);
    if (resolved !== storageRoot && !resolved.startsWith(`${storageRoot}${path.sep}`)) {
      throw new Error('存储路径越界');
    }
    return resolved;
  };

  const listCandidates = async (userId, username) => {
    const candidates = [];

    const [uploads] = await pool.query(
      `SELECT id, filename, reserved_bytes, temporary_storage_key, status, expires_at, created_at
       FROM h5_upload_sessions
       WHERE user_id = ? AND status IN ('reserved', 'uploaded', 'expired', 'failed')
       ORDER BY created_at DESC
       LIMIT 100`,
      [userId],
    );
    for (const upload of uploads) {
      const storagePath = upload.temporary_storage_key
        ? absoluteStoragePath(upload.temporary_storage_key)
        : null;
      const exists = storagePath ? await fileSize(storagePath) : 0;
      if (!exists && upload.status === 'expired') continue;
      candidates.push({
        id: candidateId('upload', upload.id),
        kind: 'stale_upload',
        label: upload.filename,
        path: upload.temporary_storage_key ?? '',
        sizeBytes: exists || Number(upload.reserved_bytes ?? 0),
        createdAt: Number(upload.created_at),
        detail:
          upload.status === 'reserved'
            ? '未完成的上传预留'
            : upload.status === 'uploaded'
              ? '已上传但未完成入库'
              : '已失效的上传临时文件',
        refId: upload.id,
      });
    }

    const tmpDir = path.join(storageRoot, 'tmp', userId);
    await walkFiles(tmpDir, async (fullPath) => {
      const key = path.relative(storageRoot, fullPath).split(path.sep).join('/');
      const active = uploads.some(
        (upload) => upload.temporary_storage_key === key && upload.status === 'reserved',
      );
      if (active) return;
      const sizeBytes = await fileSize(fullPath);
      if (!sizeBytes) return;
      candidates.push({
        id: candidateId('tmp', key),
        kind: 'orphan_tmp',
        label: path.basename(fullPath),
        path: key,
        sizeBytes,
        createdAt: null,
        detail: '孤立的临时上传文件',
        refId: key,
      });
    });

    const workspaceTempDir = path.join(h5Root, 'temp', username);
    await walkFiles(workspaceTempDir, async (fullPath) => {
      const rel = path.relative(workspaceTempDir, fullPath).split(path.sep).join('/');
      const sizeBytes = await fileSize(fullPath);
      if (!sizeBytes) return;
      candidates.push({
        id: candidateId('workspace', rel),
        kind: 'workspace_temp',
        label: rel,
        path: `temp/${username}/${rel}`,
        sizeBytes,
        createdAt: null,
        detail: 'Agent 工作区临时文件',
        refId: rel,
      });
    });

    const [staleVersions] = await pool.query(
      `SELECT pv.id AS version_id, pv.page_id, pv.version_no, pv.content_asset_id, pv.bundle_asset_id,
              p.title, a.size_bytes, a.display_name, pv.created_at,
              COALESCE(bundle.size_bytes, 0) AS bundle_size_bytes
       FROM h5_page_versions pv
       JOIN h5_page_records p ON p.id = pv.page_id AND p.user_id = ? AND p.status <> 'deleted'
       JOIN h5_assets a ON a.id = pv.content_asset_id AND a.user_id = ? AND a.status <> 'deleted'
       LEFT JOIN h5_assets bundle
         ON bundle.id = pv.bundle_asset_id AND bundle.user_id = ? AND bundle.status <> 'deleted'
       WHERE pv.id <> p.current_version_id
         AND NOT EXISTS (
           SELECT 1 FROM h5_publish_records pr
           WHERE pr.page_version_id = pv.id AND pr.user_id = ?
         )
       ORDER BY pv.created_at ASC
       LIMIT 500`,
      [userId, userId, userId, userId],
    );
    for (const row of staleVersions) {
      const contentBytes = Number(row.size_bytes ?? 0);
      const bundleBytes = Number(row.bundle_size_bytes ?? 0);
      candidates.push({
        id: candidateId('page_version', row.version_id),
        kind: 'stale_page_version',
        label: `${row.title} · v${row.version_no}`,
        path: `page/${row.page_id}/version/${row.version_no}`,
        sizeBytes: contentBytes + bundleBytes,
        createdAt: Number(row.created_at),
        detail: '页面历史版本（非当前版本）',
        refId: row.version_id,
      });
    }

    return candidates;
  };

  const runCleanup = async (userId, username, itemIds) => {
    const selected = new Set(itemIds ?? []);
    const candidates = await listCandidates(userId, username);
    const targets = candidates.filter((item) => selected.has(item.id));
    let freedBytes = 0;
    let removedCount = 0;

    for (const item of targets) {
      if (item.kind === 'stale_upload') {
        const conn = await pool.getConnection();
        try {
          await conn.beginTransaction();
          const [rows] = await conn.query(
            `SELECT id, space_id, reserved_bytes, temporary_storage_key, status
             FROM h5_upload_sessions
             WHERE id = ? AND user_id = ? AND status IN ('reserved', 'uploaded', 'expired', 'failed')
             LIMIT 1 FOR UPDATE`,
            [item.refId, userId],
          );
          const upload = rows[0];
          if (upload) {
            await conn.query(
              `UPDATE h5_user_spaces
               SET reserved_bytes = GREATEST(0, reserved_bytes - ?), updated_at = ?
               WHERE id = ? AND user_id = ?`,
              [upload.reserved_bytes, Date.now(), upload.space_id, userId],
            );
            await conn.query(
              `UPDATE h5_upload_sessions SET status = 'expired' WHERE id = ? AND user_id = ?`,
              [upload.id, userId],
            );
            await conn.commit();
            if (upload.temporary_storage_key) {
              const target = absoluteStoragePath(upload.temporary_storage_key);
              const sizeBytes = await fileSize(target);
              await fs.rm(target, { force: true });
              freedBytes += sizeBytes;
            }
            removedCount += 1;
          } else {
            await conn.rollback();
          }
        } catch {
          await conn.rollback();
        } finally {
          conn.release();
        }
        continue;
      }

      if (item.kind === 'orphan_tmp') {
        const target = absoluteStoragePath(item.refId);
        const sizeBytes = await fileSize(target);
        await fs.rm(target, { force: true });
        freedBytes += sizeBytes;
        removedCount += 1;
        continue;
      }

      if (item.kind === 'workspace_temp') {
        const target = path.join(h5Root, 'temp', username, item.refId);
        const resolvedRoot = path.resolve(path.join(h5Root, 'temp', username));
        const resolved = path.resolve(target);
        if (!resolved.startsWith(`${resolvedRoot}${path.sep}`)) continue;
        const sizeBytes = await fileSize(resolved);
        await fs.rm(resolved, { force: true });
        freedBytes += sizeBytes;
        removedCount += 1;
        continue;
      }

      if (item.kind === 'stale_page_version') {
        const conn = await pool.getConnection();
        try {
          await conn.beginTransaction();
          const [rows] = await conn.query(
            `SELECT pv.id, pv.page_id, pv.content_asset_id, pv.bundle_asset_id,
                    p.current_version_id, p.space_id
             FROM h5_page_versions pv
             JOIN h5_page_records p ON p.id = pv.page_id
             WHERE pv.id = ? AND p.user_id = ?
             LIMIT 1 FOR UPDATE`,
            [item.refId, userId],
          );
          const version = rows[0];
          if (!version || version.id === version.current_version_id) {
            await conn.rollback();
            continue;
          }
          const [pubRows] = await conn.query(
            `SELECT id FROM h5_publish_records WHERE page_version_id = ? AND user_id = ? LIMIT 1`,
            [version.id, userId],
          );
          if (pubRows.length) {
            await conn.rollback();
            continue;
          }

          const now = Date.now();
          let freed = 0;
          const assetsToDelete = [version.content_asset_id].filter(Boolean);
          if (version.bundle_asset_id) {
            const [bundleUse] = await conn.query(
              `SELECT COUNT(*) AS cnt FROM h5_page_versions
               WHERE bundle_asset_id = ? AND id <> ?`,
              [version.bundle_asset_id, version.id],
            );
            if (Number(bundleUse[0]?.cnt ?? 0) === 0) {
              assetsToDelete.push(version.bundle_asset_id);
            }
          }

          for (const assetId of assetsToDelete) {
            const [assetRows] = await conn.query(
              `SELECT id, size_bytes FROM h5_assets
               WHERE id = ? AND user_id = ? AND status <> 'deleted'
               LIMIT 1 FOR UPDATE`,
              [assetId, userId],
            );
            const asset = assetRows[0];
            if (!asset) continue;
            await conn.query(
              `UPDATE h5_assets SET status = 'deleted', deleted_at = ?, updated_at = ?
               WHERE id = ? AND user_id = ?`,
              [now, now, assetId, userId],
            );
            freed += Number(asset.size_bytes ?? 0);
          }

          await conn.query(`DELETE FROM h5_page_versions WHERE id = ?`, [version.id]);
          if (freed > 0) {
            await conn.query(
              `UPDATE h5_user_spaces
               SET used_bytes = GREATEST(0, used_bytes - ?), updated_at = ?
               WHERE id = ? AND user_id = ?`,
              [freed, now, version.space_id, userId],
            );
          }
          await conn.commit();
          freedBytes += freed;
          removedCount += 1;
        } catch {
          await conn.rollback();
        } finally {
          conn.release();
        }
      }
    }

    return { removedCount, freedBytes };
  };

  return { listCandidates, runCleanup };
}
