const { app, BrowserWindow, dialog, ipcMain } = require('electron'); const path = require('path'); const fs = require('fs'); const os = require('os'); const http = require('http'); const https = require('https'); const { execFile } = require('child_process'); const state = { hasWget: false, }; function detectWget() { return new Promise((resolve) => { const cmd = process.platform === 'win32' ? 'where' : 'which'; execFile(cmd, ['wget'], (err, stdout) => { if (err) { resolve(false); return; } resolve(Boolean(stdout && stdout.trim())); }); }); } function createWindow() { const win = new BrowserWindow({ width: 1200, height: 800, webPreferences: { preload: path.join(__dirname, 'preload.js'), contextIsolation: true, nodeIntegration: false, }, }); win.loadFile(path.join(__dirname, 'index.html')); } function logToRenderer(message) { const windows = BrowserWindow.getAllWindows(); if (!windows.length) return; windows[0].webContents.send('log', message); } function progressToRenderer(payload) { const windows = BrowserWindow.getAllWindows(); if (!windows.length) return; windows[0].webContents.send('progress', payload); } function listLocalDir(dirPath) { const entries = fs.readdirSync(dirPath, { withFileTypes: true }); return entries.map((entry) => { const fullPath = path.join(dirPath, entry.name); let size = 0; if (!entry.isDirectory()) { try { size = fs.statSync(fullPath).size; } catch (err) { size = 0; } } return { name: entry.name, path: fullPath, isDir: entry.isDirectory(), size, }; }); } function fetchUrl(url) { const client = url.startsWith('https') ? https : http; return new Promise((resolve, reject) => { const req = client.get(url, (res) => { if (res.statusCode !== 200) { reject(new Error(`HTTP ${res.statusCode}`)); res.resume(); return; } let data = ''; res.setEncoding('utf8'); res.on('data', (chunk) => { data += chunk; }); res.on('end', () => resolve(data)); }); req.on('error', reject); }); } function parseSizeToken(token) { if (!token || token === '-') return null; const match = token.trim().match(/^([\d.]+)\s*([KMGTP]?)(?:B)?$/i); if (!match) return null; const value = Number(match[1]); if (Number.isNaN(value)) return null; const unit = match[2] ? match[2].toUpperCase() : ''; const multipliers = { '': 1, K: 1024, M: 1024 ** 2, G: 1024 ** 3, T: 1024 ** 4, P: 1024 ** 5, }; const multiplier = multipliers[unit]; if (!multiplier) return null; return Math.round(value * multiplier); } function parseApacheIndex(html, baseUrl) { const results = []; const preMatch = html.match(/
[\s\S]*?<\/pre>/i);
  const block = preMatch ? preMatch[0] : html;
  const preLines = preMatch ? preMatch[0].split(/\r?\n/) : block.split(/\r?\n/);
  const linkRegex = /([^<]+)<\/a>/i;
  for (const line of preLines) {
    const match = line.match(linkRegex);
    if (!match) continue;
    const href = match[1];
    const anchorText = match[2];
    if (href === '../' || anchorText === '../') continue;
    const isDir = href.endsWith('/');
    const fullUrl = new URL(href, baseUrl).toString();
    let name = anchorText;
    try {
      const urlObj = new URL(href, baseUrl);
      let pathname = decodeURIComponent(urlObj.pathname);
      if (pathname.endsWith('/')) {
        pathname = pathname.slice(0, -1);
      }
      const derived = pathname.split('/').pop();
      if (derived) {
        name = isDir ? `${derived}/` : derived;
      }
    } catch (err) {
      name = anchorText;
    }
    let size = '-';
    let sizeBytes = null;
    const rest = line.slice(match.index + match[0].length).trim();
    if (rest) {
      const tokens = rest.split(/\s+/);
      const sizeToken = tokens[tokens.length - 1];
      if (sizeToken && sizeToken !== '-') {
        size = sizeToken;
        sizeBytes = parseSizeToken(sizeToken);
      }
    }
    results.push({ name, href: fullUrl, isDir, size, sizeBytes });
  }
  return results;
}

async function collectRemoteFiles(baseUrl, relDir) {
  const html = await fetchUrl(baseUrl);
  const items = parseApacheIndex(html, baseUrl);
  const files = [];
  for (const item of items) {
    const cleanName = item.name.replace(/\/$/, '');
    const nextRel = relDir ? path.join(relDir, cleanName) : cleanName;
    if (item.isDir) {
      const childFiles = await collectRemoteFiles(item.href, nextRel);
      files.push(...childFiles);
    } else {
      files.push({
        url: item.href,
        relPath: nextRel,
        sizeBytes: item.sizeBytes ?? null,
      });
    }
  }
  return files;
}

function downloadFile(url, destPath, onProgress) {
  const client = url.startsWith('https') ? https : http;
  return new Promise((resolve, reject) => {
    const fileStream = fs.createWriteStream(destPath);
    const req = client.get(url, (res) => {
      if (res.statusCode !== 200) {
        reject(new Error(`HTTP ${res.statusCode}`));
        res.resume();
        return;
      }
      const totalBytes = Number(res.headers['content-length']) || null;
      let downloaded = 0;
      let lastEmit = Date.now();
      onProgress?.({ phase: 'start', totalBytes });
      res.pipe(fileStream);
      res.on('data', (chunk) => {
        downloaded += chunk.length;
        const now = Date.now();
        if (onProgress && (now - lastEmit > 200 || downloaded === totalBytes)) {
          lastEmit = now;
          onProgress({
            phase: 'progress',
            downloadedBytes: downloaded,
            totalBytes,
          });
        }
      });
      fileStream.on('finish', () => {
        fileStream.close(resolve);
      });
    });
    req.on('error', (err) => {
      fs.unlink(destPath, () => reject(err));
    });
  });
}

function getRemoteBasename(url) {
  try {
    const urlObj = new URL(url);
    let pathname = decodeURIComponent(urlObj.pathname);
    if (pathname.endsWith('/')) {
      pathname = pathname.slice(0, -1);
    }
    const name = pathname.split('/').pop();
    return name || 'download';
  } catch (err) {
    return 'download';
  }
}

async function downloadFolderWithPlan(baseUrl, destDir) {
  progressToRenderer({ phase: 'scan-start' });
  const files = await collectRemoteFiles(baseUrl, '');
  progressToRenderer({
    phase: 'scan-done',
    fileCount: files.length,
  });
  const totalBytes = files.every((file) => typeof file.sizeBytes === 'number')
    ? files.reduce((sum, file) => sum + file.sizeBytes, 0)
    : null;
  progressToRenderer({
    phase: 'aggregate-start',
    totalBytes,
    fileCount: files.length,
  });

  let completedBytes = 0;
  for (const file of files) {
    const targetPath = path.join(destDir, file.relPath);
    const targetDir = path.dirname(targetPath);
    fs.mkdirSync(targetDir, { recursive: true });
    logToRenderer(`Downloading ${file.url}`);
    progressToRenderer({ phase: 'file-start', fileName: file.relPath });
    await downloadFile(file.url, targetPath, (progress) => {
      const current = progress.downloadedBytes || 0;
      const aggregate = completedBytes + current;
      progressToRenderer({
        phase: 'file-progress',
        fileName: file.relPath,
        downloadedBytes: current,
        totalBytes: progress.totalBytes,
      });
      progressToRenderer({
        phase: 'aggregate-progress',
        downloadedBytes: aggregate,
        totalBytes,
      });
    });
    completedBytes += file.sizeBytes ?? 0;
    progressToRenderer({ phase: 'file-done', fileName: file.relPath });
  }
}

async function downloadWithWget(url, destDir) {
  return new Promise((resolve, reject) => {
    const args = ['-r', '-np', '-nH', '--cut-dirs=0', '-P', destDir, url];
    const proc = execFile('wget', args, (err) => {
      if (err) {
        reject(err);
        return;
      }
      resolve();
    });
    proc.stdout.on('data', (data) => logToRenderer(data.toString()));
    proc.stderr.on('data', (data) => logToRenderer(data.toString()));
  });
}

app.whenReady().then(async () => {
  state.hasWget = await detectWget();
  createWindow();

  app.on('activate', () => {
    if (BrowserWindow.getAllWindows().length === 0) createWindow();
  });
});

app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') app.quit();
});

ipcMain.handle('get-capabilities', () => {
  return { hasWget: state.hasWget };
});

ipcMain.handle('select-local-dir', async () => {
  const result = await dialog.showOpenDialog({ properties: ['openDirectory'] });
  if (result.canceled || result.filePaths.length === 0) return null;
  return result.filePaths[0];
});

ipcMain.handle('list-local-dir', (_event, dirPath) => {
  return listLocalDir(dirPath);
});

ipcMain.handle('list-remote-dir', async (_event, url) => {
  const html = await fetchUrl(url);
  return parseApacheIndex(html, url);
});

ipcMain.handle('download-item', async (_event, payload) => {
  const { url, destDir, isDir, useWget } = payload;
  if (!fs.existsSync(destDir)) {
    fs.mkdirSync(destDir, { recursive: true });
  }
  if (isDir) {
    const folderName = getRemoteBasename(url);
    const targetDir = path.join(destDir, folderName);
    if (!fs.existsSync(targetDir)) {
      fs.mkdirSync(targetDir, { recursive: true });
    }
    if (useWget && state.hasWget) {
      logToRenderer(`wget: ${url}`);
      progressToRenderer({ phase: 'start', mode: 'wget', fileName: folderName });
      await downloadWithWget(url, targetDir);
      progressToRenderer({ phase: 'done' });
      return;
    }
    await downloadFolderWithPlan(url, targetDir);
    progressToRenderer({ phase: 'done' });
    return;
  }
  const fileName = path.basename(new URL(url).pathname);
  const destPath = path.join(destDir, fileName);
  logToRenderer(`Downloading ${url}`);
  progressToRenderer({ phase: 'start', fileName });
  await downloadFile(url, destPath, (progress) => {
    progressToRenderer({
      phase: 'progress',
      fileName,
      downloadedBytes: progress.downloadedBytes,
      totalBytes: progress.totalBytes,
    });
  });
  progressToRenderer({ phase: 'done' });
});