'use strict';

const express = require('express');
const router = express.Router();
const fs = require('fs');
const path = require('path');
const { v4: uuidv4 } = require('uuid');
const multer = require('multer');
const extractZip = require('extract-zip');

const iis = require('../utils/iis');
const vm  = require('../utils/versionManager');
const log = require('../utils/logger');

const CONFIG_PATH          = path.join(__dirname, '..', 'data', 'config.json');
const DEPLOY_CONFIGS_PATH  = path.join(__dirname, '..', 'data', 'deploy-configs.json');
const UPLOADS_DIR          = path.join(__dirname, '..', 'uploads');
const BACKUPS_DIR          = path.join(__dirname, '..', 'backups');

// ─── Multer config ────────────────────────────────────────────────────────────
const storage = multer.diskStorage({
  destination: (_req, _file, cb) => cb(null, UPLOADS_DIR),
  filename: (_req, file, cb) => {
    const safe = path.basename(file.originalname).replace(/[^a-zA-Z0-9._\-]/g, '_');
    cb(null, `${Date.now()}_${safe}`);
  },
});
const upload = multer({
  storage,
  limits: { fileSize: 500 * 1024 * 1024 }, // 500 MB
  fileFilter: (_req, file, cb) => {
    // Allow zip and any file for the file-manager upload
    cb(null, true);
  },
});

// ─── Helpers ──────────────────────────────────────────────────────────────────
function readConfig() {
  return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
}
function writeConfig(cfg) {
  fs.writeFileSync(CONFIG_PATH, JSON.stringify(cfg, null, 2));
}
function findSite(id) {
  return readConfig().sites.find(s => s.id === id) || null;
}
function readDeployConfigs() {
  if (!fs.existsSync(DEPLOY_CONFIGS_PATH)) return {};
  try { return JSON.parse(fs.readFileSync(DEPLOY_CONFIGS_PATH, 'utf8')); } catch { return {}; }
}
function writeDeployConfigs(configs) {
  fs.writeFileSync(DEPLOY_CONFIGS_PATH, JSON.stringify(configs, null, 2));
}

/**
 * Safely resolve a path inside a base directory (prevents traversal).
 * Handles Windows quirk where path.resolve(base, '/') returns the drive root.
 */
function safePath(base, rel) {
  const resolvedBase = path.resolve(base);
  // Strip ALL leading slashes/backslashes so the remainder is relative to base,
  // not an absolute path that would bypass the base entirely on Windows.
  const relClean = (rel || '').replace(/^[/\\]+/, '');
  const full = relClean ? path.resolve(resolvedBase, relClean) : resolvedBase;
  // Must be the base itself OR strictly inside it (add sep to avoid C:\foo matching C:\foobar)
  if (full !== resolvedBase && !full.startsWith(resolvedBase + path.sep)) {
    throw Object.assign(new Error('Access denied: path outside site root'), { status: 403 });
  }
  return full;
}

function copyDirRecursive(src, dest, retainSet = new Set(), _relBase = '') {
  fs.mkdirSync(dest, { recursive: true });
  for (const item of fs.readdirSync(src)) {
    const s   = path.join(src, item);
    const d   = path.join(dest, item);
    const rel = _relBase ? _relBase + '/' + item : item;
    if (isRetained(rel, retainSet)) continue;
    if (fs.statSync(s).isDirectory()) copyDirRecursive(s, d, retainSet, rel);
    else fs.copyFileSync(s, d);
  }
}

/**
 * Delete all contents of `dir` except paths covered by retainSet.
 * Retained directories are entered recursively so their children are
 * also preserved, but un-retained siblings are still removed.
 */
function cleanDeployDir(dir, retainSet = new Set(), _relBase = '') {
  if (!fs.existsSync(dir)) return;
  for (const item of fs.readdirSync(dir)) {
    const full = path.join(dir, item);
    const rel  = _relBase ? _relBase + '/' + item : item;
    if (isRetained(rel, retainSet)) continue;
    const stat = fs.statSync(full);
    if (stat.isDirectory()) {
      // If any retain entry is inside this dir, recurse instead of bulk-delete
      const hasRetainedChild = [...retainSet].some(r => r.startsWith(rel.toLowerCase() + '/'));
      if (hasRetainedChild) {
        cleanDeployDir(full, retainSet, rel);
      } else {
        fs.rmSync(full, { recursive: true, force: true });
      }
    } else {
      fs.unlinkSync(full);
    }
  }
}

function isRetained(relPath, retainSet) {
  if (!retainSet.size) return false;
  const norm = relPath.replace(/\\/g, '/').toLowerCase();
  for (const r of retainSet) {
    // Exact match, or relPath starts with retained dir prefix
    if (norm === r || norm.startsWith(r.endsWith('/') ? r : r + '/')) return true;
  }
  return false;
}

// ─── Dashboard ────────────────────────────────────────────────────────────────
router.get('/dashboard', async (_req, res) => {
  const cfg = readConfig();
  const deployments = vm.getAll();
  const success = deployments.filter(d => d.status === 'success').length;
  const failed  = deployments.filter(d => d.status === 'failed').length;
  const recent  = deployments.slice(0, 8);

  // Chart data: deployments per day last 14 days
  const days = Array.from({ length: 14 }, (_, i) => {
    const d = new Date(); d.setDate(d.getDate() - (13 - i));
    return d.toISOString().slice(0, 10);
  });
  const chartData = days.map(day => ({
    day,
    count: deployments.filter(d => d.deployedAt.startsWith(day)).length,
  }));

  res.json({
    stats: {
      totalSites: cfg.sites.length,
      totalDeployments: deployments.length,
      successfulDeployments: success,
      failedDeployments: failed,
    },
    recent,
    chartData,
    activity: log.getAll(20),
  });
});

// ─── Config / Settings ────────────────────────────────────────────────────────
router.get('/config', (_req, res) => {
  res.json(readConfig());
});

router.put('/config/settings', (req, res) => {
  const cfg = readConfig();
  cfg.settings = { ...cfg.settings, ...req.body };
  writeConfig(cfg);
  log.info('system', 'Settings updated');
  res.json({ ok: true, settings: cfg.settings });
});

// ─── Managed Sites (app config) ───────────────────────────────────────────────
router.get('/sites/managed', (_req, res) => {
  res.json(readConfig().sites);
});

router.post('/sites/managed', (req, res) => {
  const { name, iisName, deployPath, appPool, description } = req.body;
  if (!name || !deployPath) {
    return res.status(400).json({ error: 'name and deployPath are required' });
  }
  const cfg = readConfig();
  const site = {
    id: uuidv4(),
    name: name.trim(),
    iisName: (iisName || name).trim(),
    deployPath: deployPath.trim(),
    appPool: (appPool || '').trim(),
    description: (description || '').trim(),
    createdAt: new Date().toISOString(),
  };
  cfg.sites.push(site);
  writeConfig(cfg);
  log.success('system', `Site "${site.name}" registered`);
  res.status(201).json(site);
});

router.put('/sites/managed/:id', (req, res) => {
  const cfg = readConfig();
  const idx = cfg.sites.findIndex(s => s.id === req.params.id);
  if (idx === -1) return res.status(404).json({ error: 'Site not found' });
  cfg.sites[idx] = { ...cfg.sites[idx], ...req.body, id: cfg.sites[idx].id };
  writeConfig(cfg);
  log.info('system', `Site "${cfg.sites[idx].name}" updated`);
  res.json(cfg.sites[idx]);
});

router.delete('/sites/managed/:id', (req, res) => {
  const cfg = readConfig();
  const idx = cfg.sites.findIndex(s => s.id === req.params.id);
  if (idx === -1) return res.status(404).json({ error: 'Site not found' });
  const [removed] = cfg.sites.splice(idx, 1);
  writeConfig(cfg);
  log.warning('system', `Site "${removed.name}" removed from DeployPro`);
  res.json({ ok: true });
});

// ─── Import managed sites from live IIS ──────────────────────────────────────
router.post('/sites/import-from-iis', async (_req, res) => {
  const result = await iis.getSites();
  if (!result.ok) {
    return res.status(503).json({ error: result.error || 'IIS not accessible — ensure server runs as Administrator' });
  }

  const cfg = readConfig();
  const existing = new Set(cfg.sites.map(s => s.iisName.toLowerCase()));

  const imported = [];
  for (const iisSite of result.data) {
    if (existing.has(iisSite.Name.toLowerCase())) continue;

    let appPool = '';
    try { appPool = (await iis.getPoolForSite(iisSite.Name)) || ''; } catch { /* ignore */ }

    const site = {
      id: uuidv4(),
      name: iisSite.Name,
      iisName: iisSite.Name,
      deployPath: iisSite.PhysicalPath || '',
      appPool,
      description: 'Imported from IIS',
      createdAt: new Date().toISOString(),
    };
    cfg.sites.push(site);
    imported.push(site);
  }

  if (imported.length > 0) {
    writeConfig(cfg);
    log.success('system', `Imported ${imported.length} site(s) from IIS`);
  }

  res.json({ ok: true, imported: imported.length, sites: imported });
});

// ─── IIS Sites (live) ─────────────────────────────────────────────────────────
router.get('/iis/sites', async (_req, res) => {
  const result = await iis.getSites();
  if (!result.ok && result.data.length === 0) {
    // Return a friendly stub when IIS is unavailable (dev mode)
    return res.json({ ok: false, data: [], error: result.error });
  }
  res.json(result);
});

router.post('/iis/sites/:name/start', async (req, res) => {
  const r = await iis.controlSite(req.params.name, 'Start');
  log.success('iis', `Site "${req.params.name}" started`);
  res.json(r);
});

router.post('/iis/sites/:name/stop', async (req, res) => {
  const r = await iis.controlSite(req.params.name, 'Stop');
  log.warning('iis', `Site "${req.params.name}" stopped`);
  res.json(r);
});

// ─── IIS App Pools ────────────────────────────────────────────────────────────
router.get('/iis/pools', async (_req, res) => {
  const result = await iis.getAppPools();
  res.json(result);
});

router.post('/iis/pools/:name/start', async (req, res) => {
  const r = await iis.controlPool(req.params.name, 'Start');
  log.success('iis', `Pool "${req.params.name}" started`);
  res.json(r);
});

router.post('/iis/pools/:name/stop', async (req, res) => {
  const r = await iis.controlPool(req.params.name, 'Stop');
  log.warning('iis', `Pool "${req.params.name}" stopped`);
  res.json(r);
});

router.post('/iis/pools/:name/recycle', async (req, res) => {
  const r = await iis.controlPool(req.params.name, 'Recycle');
  log.info('iis', `Pool "${req.params.name}" recycled`);
  res.json(r);
});

// ─── IIS Reset ────────────────────────────────────────────────────────────────
router.post('/iis/reset', async (_req, res) => {
  log.warning('iis', 'IIS reset initiated…');
  global.broadcastLog('IIS reset initiated…', 'warning');
  const r = await iis.iisReset();
  if (r.success) {
    log.success('iis', 'IIS reset completed');
    global.broadcastLog('IIS reset completed', 'success');
  } else {
    log.error('iis', `IIS reset failed: ${r.stderr}`);
    global.broadcastLog(`IIS reset failed: ${r.stderr}`, 'error');
  }
  res.json(r);
});

// ─── Versions ────────────────────────────────────────────────────────────────
router.get('/versions', (_req, res) => {
  res.json(vm.getAll());
});

router.get('/versions/:siteId', (req, res) => {
  res.json(vm.getBySite(req.params.siteId));
});

// ─── Upload package ──────────────────────────────────────────────────────────
router.post('/upload', upload.single('package'), (req, res) => {
  if (!req.file) return res.status(400).json({ error: 'No file uploaded' });
  res.json({ ok: true, filename: req.file.filename, path: req.file.path });
});
// ─── Upload multiple files (files mode or folder mode) ────────────────
router.post('/upload-multi', upload.array('files', 1000), (req, res) => {
  if (!req.files || !req.files.length) return res.status(400).json({ error: 'No files uploaded' });
  let paths;
  try { paths = JSON.parse(req.body.paths || '[]'); } catch { paths = []; }

  const uploadId = `multi_${Date.now()}`;
  const tempDir  = path.join(UPLOADS_DIR, uploadId);
  fs.mkdirSync(tempDir, { recursive: true });

  req.files.forEach((f, i) => {
    // Use provided relative path, sanitise to prevent traversal
    let rel = (paths[i] || f.originalname).replace(/\.\./g, '_').replace(/^[\/\\]+/, '');
    if (!rel) rel = f.originalname;
    const dest = path.join(tempDir, rel);
    // Ensure dest is inside tempDir
    if (!dest.startsWith(tempDir + path.sep) && dest !== tempDir) {
      fs.unlinkSync(f.path); // discard unsafe file
      return;
    }
    fs.mkdirSync(path.dirname(dest), { recursive: true });
    fs.renameSync(f.path, dest);
  });

  res.json({ ok: true, uploadId, fileCount: req.files.length });
});

// ─── Deploy ───────────────────────────────────────────────────────────────────
router.post('/deploy', async (req, res) => {
  const {
    siteId,
    version,
    notes,
    packageFilename,  // uploaded zip filename
    uploadId,         // multi-file/folder upload session id
    retainPaths,      // paths to keep unchanged
    iisResetAfter,
    autoBackup,
    stopPool,
  } = req.body;

  const site = findSite(siteId);
  if (!site) return res.status(404).json({ error: 'Site not found' });
  if (!version) return res.status(400).json({ error: 'version is required' });
  if (!packageFilename && !uploadId) return res.status(400).json({ error: 'No package provided' });

  // Build retain set – normalise slashes, lowercase
  const rawRetain = Array.isArray(retainPaths) ? retainPaths : (retainPaths ? [retainPaths] : []);
  const retainSet = new Set(
    rawRetain.map(p => p.replace(/\\/g, '/').replace(/^\/+/, '').toLowerCase()).filter(Boolean)
  );

  const cfg = readConfig();
  const deployPath = site.deployPath;

  if (!fs.existsSync(deployPath)) {
    return res.status(400).json({ error: `Deploy path does not exist: ${deployPath}` });
  }

  const deployId = uuidv4();
  res.json({ ok: true, deployId, message: 'Deployment started' });

  // ── Run deployment asynchronously ──────────────────────────────
  (async () => {
    const broadcast = (msg, level = 'info') => {
      global.broadcastLog(msg, level);
      log[level] || log.info;
    };

    const entry = vm.recordDeployment({
      siteId, siteName: site.name, version, notes,
      status: 'in-progress', isRollback: false,
    });

    broadcast(`[Deploy] Starting deployment of ${site.name} v${version}`, 'info');
    global.broadcastEvent('deployStart', { siteId, version });

    let backupPath = null;

    try {
      // 1. Backup
      if (autoBackup !== 'false' && autoBackup !== false) {
        broadcast('[Deploy] Creating backup of current deployment…', 'info');
        backupPath = await vm.createBackup(siteId, site.name, deployPath, version);
        broadcast(`[Deploy] Backup saved: ${backupPath}`, 'success');
      }

      // 2. Stop app pool
      if ((stopPool !== 'false' && stopPool !== false) && site.appPool) {
        broadcast(`[Deploy] Stopping app pool: ${site.appPool}…`, 'info');
        await iis.controlPool(site.appPool, 'Stop');
      }

      // 3. Clean deploy directory (remove old files, keep retained)
      broadcast('[Deploy] Cleaning deploy directory…', 'info');
      if (retainSet.size) broadcast(`[Deploy] Retaining: ${[...retainSet].join(', ')}`, 'info');
      cleanDeployDir(deployPath, retainSet);

      // 4. Extract / copy new files
      if (packageFilename) {
        const pkgPath = path.join(UPLOADS_DIR, packageFilename);
        if (!fs.existsSync(pkgPath)) throw new Error('Uploaded package not found on server');
        const extractDir = path.join(UPLOADS_DIR, `extract_${Date.now()}`);
        broadcast('[Deploy] Extracting package…', 'info');
        await extractZip(pkgPath, { dir: extractDir });
        broadcast('[Deploy] Copying files to deploy path…', 'info');
        copyDirRecursive(extractDir, deployPath, retainSet);
        fs.rmSync(extractDir, { recursive: true, force: true });
        fs.unlinkSync(pkgPath);
        broadcast('[Deploy] Files deployed successfully', 'success');
      } else if (uploadId) {
        const tempDir = path.join(UPLOADS_DIR, uploadId);
        if (!fs.existsSync(tempDir)) throw new Error('Upload session not found — please re-upload');
        broadcast('[Deploy] Copying files to deploy path…', 'info');
        copyDirRecursive(tempDir, deployPath, retainSet);
        fs.rmSync(tempDir, { recursive: true, force: true });
        broadcast('[Deploy] Files deployed successfully', 'success');
      }

      // 4. Start app pool
      if ((stopPool !== 'false' && stopPool !== false) && site.appPool) {
        broadcast(`[Deploy] Starting app pool: ${site.appPool}…`, 'info');
        await iis.controlPool(site.appPool, 'Start');
      }

      // 5. IIS Reset if requested
      if (iisResetAfter === 'true' || iisResetAfter === true) {
        broadcast('[Deploy] Performing IIS reset…', 'warning');
        const r = await iis.iisReset();
        broadcast(r.success ? '[Deploy] IIS reset complete' : `[Deploy] IIS reset warning: ${r.stderr}`,
          r.success ? 'success' : 'warning');
      }

      vm.updateDeploymentStatus(entry.id, 'success', { backupPath });
      vm.pruneOldVersions(siteId, cfg.settings.maxVersions || 10);

      broadcast(`[Deploy] ✓ Deployment of ${site.name} v${version} succeeded`, 'success');
      log.success('deploy', `Deployed ${site.name} v${version}`);
      global.broadcastEvent('deployComplete', { siteId, version, status: 'success' });

    } catch (err) {
      // Attempt to restart pool if we stopped it
      if ((stopPool !== 'false' && stopPool !== false) && site.appPool) {
        await iis.controlPool(site.appPool, 'Start').catch(() => {});
      }
      vm.updateDeploymentStatus(entry.id, 'failed');
      broadcast(`[Deploy] ✗ Deployment failed: ${err.message}`, 'error');
      log.error('deploy', `Deploy failed: ${err.message}`, { siteId });
      global.broadcastEvent('deployComplete', { siteId, version, status: 'failed' });
    }
  })();
});

// ─── Rollback ────────────────────────────────────────────────────────────────
router.post('/rollback', async (req, res) => {
  const { deploymentId, stopPool, iisResetAfter } = req.body;
  const dep = vm.getById(deploymentId);
  if (!dep) return res.status(404).json({ error: 'Deployment record not found' });
  if (!dep.backupPath || !fs.existsSync(dep.backupPath)) {
    return res.status(400).json({ error: 'Backup file not found for this version' });
  }

  const site = findSite(dep.siteId);
  if (!site) return res.status(404).json({ error: 'Site not registered' });

  res.json({ ok: true, message: 'Rollback started' });

  (async () => {
    const broadcast = (msg, level = 'info') => global.broadcastLog(msg, level);
    broadcast(`[Rollback] Rolling back ${site.name} to v${dep.version}…`, 'warning');
    global.broadcastEvent('rollbackStart', { siteId: dep.siteId, version: dep.version });

    try {
      // Stop pool
      if ((stopPool !== 'false' && stopPool !== false) && site.appPool) {
        broadcast(`[Rollback] Stopping pool ${site.appPool}…`, 'info');
        await iis.controlPool(site.appPool, 'Stop');
      }

      // Extract backup zip over deploy path
      const extractDir = path.join(UPLOADS_DIR, `rollback_${Date.now()}`);
      broadcast('[Rollback] Extracting backup archive…', 'info');
      await extractZip(dep.backupPath, { dir: extractDir });
      broadcast('[Rollback] Restoring files…', 'info');
      copyDirRecursive(extractDir, site.deployPath);
      fs.rmSync(extractDir, { recursive: true, force: true });

      // Start pool
      if ((stopPool !== 'false' && stopPool !== false) && site.appPool) {
        broadcast(`[Rollback] Starting pool ${site.appPool}…`, 'info');
        await iis.controlPool(site.appPool, 'Start');
      }

      if (iisResetAfter === 'true' || iisResetAfter === true) {
        broadcast('[Rollback] Performing IIS reset…', 'warning');
        await iis.iisReset();
      }

      vm.recordDeployment({
        siteId: dep.siteId, siteName: site.name,
        version: dep.version, notes: `Rollback to v${dep.version}`,
        status: 'success', isRollback: true, rolledBackFrom: deploymentId,
      });
      log.success('deploy', `Rolled back ${site.name} to v${dep.version}`);
      broadcast(`[Rollback] ✓ Rollback to v${dep.version} complete`, 'success');
      global.broadcastEvent('rollbackComplete', { siteId: dep.siteId, status: 'success' });
    } catch (err) {
      if ((stopPool !== 'false' && stopPool !== false) && site.appPool) {
        await iis.controlPool(site.appPool, 'Start').catch(() => {});
      }
      log.error('deploy', `Rollback failed: ${err.message}`);
      broadcast(`[Rollback] ✗ Failed: ${err.message}`, 'error');
      global.broadcastEvent('rollbackComplete', { siteId: dep.siteId, status: 'failed' });
    }
  })();
});

// ─── Deploy Config (save / load per site) ───────────────────────────────────
router.get('/deploy-config/:siteId', (req, res) => {
  const configs = readDeployConfigs();
  res.json(configs[req.params.siteId] || {});
});

router.post('/deploy-config/:siteId', (req, res) => {
  const site = findSite(req.params.siteId);
  if (!site) return res.status(404).json({ error: 'Site not found' });
  const configs = readDeployConfigs();
  configs[req.params.siteId] = { ...req.body, savedAt: new Date().toISOString() };
  writeDeployConfigs(configs);
  res.json({ ok: true });
});

// ─── File Manager ─────────────────────────────────────────────────────────────

// Browse directory tree
router.get('/files/:siteId/browse', (req, res) => {
  const site = findSite(req.params.siteId);
  if (!site) return res.status(404).json({ error: 'Site not found' });

  let target;
  try { target = safePath(site.deployPath, req.query.path || '/'); }
  catch (e) { return res.status(e.status || 400).json({ error: e.message }); }

  if (!fs.existsSync(target)) return res.status(404).json({ error: 'Path not found' });

  try {
    const items = fs.readdirSync(target).map(name => {
      const full = path.join(target, name);
      let stat;
      try { stat = fs.statSync(full); } catch { return null; }
      const relPath = path.relative(site.deployPath, full).replace(/\\/g, '/');
      return {
        name,
        path: '/' + relPath,
        isDir: stat.isDirectory(),
        size: stat.isDirectory() ? null : stat.size,
        modified: stat.mtime.toISOString(),
      };
    }).filter(Boolean).sort((a, b) => {
      if (a.isDir !== b.isDir) return a.isDir ? -1 : 1;
      return a.name.localeCompare(b.name);
    });
    res.json({ path: req.query.path || '/', items, base: site.deployPath });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

// Read a file's content
router.get('/files/:siteId/read', (req, res) => {
  const site = findSite(req.params.siteId);
  if (!site) return res.status(404).json({ error: 'Site not found' });

  let target;
  try { target = safePath(site.deployPath, req.query.path || ''); }
  catch (e) { return res.status(e.status || 400).json({ error: e.message }); }

  if (!fs.existsSync(target)) return res.status(404).json({ error: 'File not found' });
  if (fs.statSync(target).isDirectory()) return res.status(400).json({ error: 'Path is a directory' });

  const stat = fs.statSync(target);
  if (stat.size > 5 * 1024 * 1024) return res.status(413).json({ error: 'File too large to edit (>5MB)' });

  try {
    const content = fs.readFileSync(target, 'utf8');
    res.json({ content, encoding: 'utf8' });
  } catch {
    // Binary – return base64
    const content = fs.readFileSync(target).toString('base64');
    res.json({ content, encoding: 'base64' });
  }
});

// Write / update a file
router.put('/files/:siteId/write', (req, res) => {
  const site = findSite(req.params.siteId);
  if (!site) return res.status(404).json({ error: 'Site not found' });

  let target;
  try { target = safePath(site.deployPath, req.body.path || ''); }
  catch (e) { return res.status(e.status || 400).json({ error: e.message }); }

  const { content } = req.body;
  if (content === undefined) return res.status(400).json({ error: 'content required' });

  try {
    fs.mkdirSync(path.dirname(target), { recursive: true });
    fs.writeFileSync(target, content, 'utf8');
    log.success('file', `Updated file ${req.body.path} in ${site.name}`);
    res.json({ ok: true });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

// Upload a file into the site directory
const fileUpload = multer({ storage, limits: { fileSize: 100 * 1024 * 1024 } });
router.post('/files/:siteId/upload', fileUpload.single('file'), (req, res) => {
  const site = findSite(req.params.siteId);
  if (!site) return res.status(404).json({ error: 'Site not found' });
  if (!req.file) return res.status(400).json({ error: 'No file provided' });

  const destRelPath = req.body.destPath || '/';
  let destDir;
  try { destDir = safePath(site.deployPath, destRelPath); }
  catch (e) { return res.status(e.status || 400).json({ error: e.message }); }

  try {
    fs.mkdirSync(destDir, { recursive: true });
    const origName = req.file.originalname.replace(/[^a-zA-Z0-9._\-]/g, '_');
    const dest = path.join(destDir, origName);
    fs.renameSync(req.file.path, dest);
    log.success('file', `Uploaded ${origName} to ${destRelPath} in ${site.name}`);
    res.json({ ok: true, path: path.relative(site.deployPath, dest).replace(/\\/g, '/') });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

// Delete file or folder
router.delete('/files/:siteId/delete', (req, res) => {
  const site = findSite(req.params.siteId);
  if (!site) return res.status(404).json({ error: 'Site not found' });

  let target;
  try { target = safePath(site.deployPath, req.body.path || ''); }
  catch (e) { return res.status(e.status || 400).json({ error: e.message }); }

  // Safety: never delete the root deploy path itself
  if (path.resolve(target) === path.resolve(site.deployPath)) {
    return res.status(403).json({ error: 'Cannot delete site root' });
  }

  try {
    fs.rmSync(target, { recursive: true, force: true });
    log.warning('file', `Deleted ${req.body.path} from ${site.name}`);
    res.json({ ok: true });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

// Create directory
router.post('/files/:siteId/mkdir', (req, res) => {
  const site = findSite(req.params.siteId);
  if (!site) return res.status(404).json({ error: 'Site not found' });

  let target;
  try { target = safePath(site.deployPath, req.body.path || ''); }
  catch (e) { return res.status(e.status || 400).json({ error: e.message }); }

  try {
    fs.mkdirSync(target, { recursive: true });
    res.json({ ok: true });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

// Rename file or folder
router.put('/files/:siteId/rename', (req, res) => {
  const site = findSite(req.params.siteId);
  if (!site) return res.status(404).json({ error: 'Site not found' });
  const { from, to } = req.body;
  if (!from || !to) return res.status(400).json({ error: 'from and to are required' });
  let srcPath, destPath;
  try { srcPath = safePath(site.deployPath, from); destPath = safePath(site.deployPath, to); }
  catch (e) { return res.status(e.status || 400).json({ error: e.message }); }
  if (!fs.existsSync(srcPath)) return res.status(404).json({ error: 'Source not found' });
  if (fs.existsSync(destPath)) return res.status(409).json({ error: 'A file with that name already exists' });
  try {
    fs.renameSync(srcPath, destPath);
    log.info('filemanager', `Renamed ${from} → ${to}`);
    res.json({ ok: true });
  } catch (err) { res.status(500).json({ error: err.message }); }
});

// Download a single file
router.get('/files/:siteId/download', (req, res) => {
  const site = findSite(req.params.siteId);
  if (!site) return res.status(404).json({ error: 'Site not found' });
  let target;
  try { target = safePath(site.deployPath, req.query.path || ''); }
  catch (e) { return res.status(e.status || 400).json({ error: e.message }); }
  if (!fs.existsSync(target) || fs.statSync(target).isDirectory())
    return res.status(404).json({ error: 'File not found' });
  const filename = path.basename(target);
  res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
  res.setHeader('Content-Type', 'application/octet-stream');
  fs.createReadStream(target).pipe(res);
});


// ─── Activity Log ─────────────────────────────────────────────────────────────
router.get('/activity', (_req, res) => {
  const limit = Math.min(parseInt(_req.query.limit) || 100, 500);
  res.json(log.getAll(limit));
});

router.delete('/activity', (_req, res) => {
  log.clear();
  res.json({ ok: true });
});

module.exports = router;
