// S.O.L.O. WorkManager Server v2.0.9 hardened local/offline + organized portable folder layout
// Robust client-session lifecycle + Professional Error Logging

const http = require('http');
const fs   = require('fs');
const path = require('path');

const APP_VERSION    = '1.0.68';
const PORT           = parseInt(process.argv[2]) || 3000;
const PROGRAM_DIR    = __dirname;
const APP_ROOT       = process.argv[4] ? path.resolve(process.argv[4]) : path.resolve(path.join(PROGRAM_DIR, '..'));
const DIST_DIR       = process.argv[3] || path.join(PROGRAM_DIR, 'dist');
const USER_DATA_DIR  = path.join(APP_ROOT, 'Benutzerdaten');
// Lifecycle policy:
// - The local server must never shut down while a browser app is still active.
// - Browser heartbeats may be delayed/throttled during tab switches, screen recording or idle time.
// - Registered clients therefore stay valid for many hours.
// - A real page close/unload unregisters the client and then the server exits after a short grace period.
// - Launcher health checks avoid opening an old/stale server from another extracted release folder.
const CLIENT_TIMEOUT_MS = 12 * 60 * 60 * 1000; // 12 h: tolerate browser timer throttling
const IDLE_SHUTDOWN_DELAY_MS = 60 * 1000;      // 60 s: close/reopen grace after last unregister
const STARTUP_GRACE_MS = 5 * 60 * 1000;       // 5 min: allow browser startup/registration
const LOG_DIR        = path.join(USER_DATA_DIR, 'Logs');
const MAX_LOG_BYTES  = 5 * 1024 * 1024; // 5 MB → rotate



// ─── Program-folder database (authoritative local data store) ───────────────
const DATA_DIR = path.join(USER_DATA_DIR, 'Datenbank');
const DB_FILE  = path.join(DATA_DIR, 'solo_workmanager_database.json');
const SAMPLE_WORKSPACE_FILE = path.join(PROGRAM_DIR, 'Beispieldaten', 'workmanager_Beispiel_WorkSpace_2026-06-03.json');
const DB_VERSION = 1;
const STORE_NAMES = ['projects','tasks','taskLogs','pspElements','timeEntries','reminders','tags','milestones','todos','deletedArchive','settings'];

function isExampleWorkspace(ws) {
  if (!ws) return false;
  const id = String(ws.id || '');
  const name = String(ws.name || '').toLowerCase();
  return ws.isExampleWorkspace === true || id === '1780252722993_0uy0824' || name.includes('beispiel') || name.includes('example workspace');
}
function normalizeWorkspaceMeta(ws) {
  if (!ws || typeof ws !== 'object') return ws;
  if (isExampleWorkspace(ws)) ws.isExampleWorkspace = true;
  return ws;
}

function ensureDataDir() {
  if (!fs.existsSync(DATA_DIR)) fs.mkdirSync(DATA_DIR, { recursive: true });
  migrateLegacyDatabaseIfNeeded();
}
function migrateLegacyDatabaseIfNeeded() {
  try {
    if (fs.existsSync(DB_FILE)) return false;
    const legacyCandidates = [
      path.join(APP_ROOT, 'data', 'solo_workmanager_database.json'),
      path.join(PROGRAM_DIR, 'data', 'solo_workmanager_database.json')
    ];
    const legacy = legacyCandidates.find((candidate) => fs.existsSync(candidate));
    if (!legacy) return false;
    fs.copyFileSync(legacy, DB_FILE);
    try {
      const marker = path.join(DATA_DIR, 'MIGRATION_v1.0.68.txt');
      fs.writeFileSync(marker, 'Datenbank wurde beim Start automatisch aus alter Struktur uebernommen:\n' + legacy + '\nZiel:\n' + DB_FILE + '\nZeit:\n' + new Date().toISOString() + '\n', 'utf8');
    } catch (_) {}
    writeLog('INFO', 'localdb', 'Legacy database migrated to Benutzerdaten/Datenbank', { source: legacy, target: DB_FILE });
    return true;
  } catch (e) {
    writeLog('WARN', 'localdb', 'Legacy database migration failed: ' + e.message);
    return false;
  }
}
function emptyWorkspaceData() { const d = {}; STORE_NAMES.forEach(k => { d[k] = []; }); return d; }
function buildInitialLocalDb() {
  const now = new Date().toISOString();
  const mainWorkspace = {
    id: 'ws_' + Date.now().toString(36) + '_' + Math.random().toString(36).slice(2, 8),
    name: 'Neuer Workspace',
    color: '#3b82f6',
    createdAt: now
  };
  const db = {
    fileFormat: 'SoloWorkManagerDatabase',
    version: DB_VERSION,
    appVersion: APP_VERSION,
    createdAt: now,
    updatedAt: now,
    meta: {
      workspaces: [mainWorkspace],
      activeWorkspaceId: mainWorkspace.id,
      localSettings: { wm_language: 'de', wm_theme: 'dark', wm_fontscale: '100' }
    },
    workspacesData: {}
  };
  db.workspacesData[mainWorkspace.id] = emptyWorkspaceData();
  try {
    if (fs.existsSync(SAMPLE_WORKSPACE_FILE)) {
      const sample = JSON.parse(fs.readFileSync(SAMPLE_WORKSPACE_FILE, 'utf8'));
      if (sample && sample.workspace && sample.workspace.id && sample.data) {
        const sw = normalizeWorkspaceMeta(Object.assign({}, sample.workspace, { name: sample.workspace.name || 'Beispiel WorkSpace', isExampleWorkspace: true }));
        if (!db.meta.workspaces.some(w => w.id === sw.id)) db.meta.workspaces.push(sw);
        db.workspacesData[sw.id] = emptyWorkspaceData();
        STORE_NAMES.forEach(st => {
          if (Array.isArray(sample.data[st])) db.workspacesData[sw.id][st] = sample.data[st];
        });
      }
    }
  } catch (e) {
    writeLog('WARN', 'localdb', 'Beispiel-Workspace konnte nicht vorgeladen werden: ' + e.message);
  }
  return db;
}
function emptyLocalDb() { return buildInitialLocalDb(); }
function ensureIntegratedSampleWorkspace(db) {
  try {
    if (!db || !fs.existsSync(SAMPLE_WORKSPACE_FILE)) return false;
    const sample = JSON.parse(fs.readFileSync(SAMPLE_WORKSPACE_FILE, 'utf8'));
    if (!sample || !sample.workspace || !sample.workspace.id || !sample.data) return false;
    db.meta = db.meta || { workspaces: [], activeWorkspaceId: null, localSettings: {} };
    db.workspacesData = db.workspacesData || {};
    const sw = normalizeWorkspaceMeta(Object.assign({}, sample.workspace, { name: sample.workspace.name || 'Beispiel WorkSpace', isExampleWorkspace: true }));
    const list = Array.isArray(db.meta.workspaces) ? db.meta.workspaces : [];
    const idx = list.findIndex(w => w && w.id === sw.id);
    let changed = false;
    if (idx >= 0) {
      if (list[idx].isExampleWorkspace !== true) { list[idx] = normalizeWorkspaceMeta(Object.assign({}, list[idx], { isExampleWorkspace: true })); changed = true; }
    } else {
      list.push(sw); changed = true;
    }
    db.meta.workspaces = list;
    const existing = db.workspacesData[sw.id];
    const hasExistingData = existing && STORE_NAMES.some(st => Array.isArray(existing[st]) && existing[st].length > 0);
    if (!hasExistingData) {
      db.workspacesData[sw.id] = emptyWorkspaceData();
      STORE_NAMES.forEach(st => { if (Array.isArray(sample.data[st])) db.workspacesData[sw.id][st] = sample.data[st]; });
      changed = true;
    }
    return changed;
  } catch (e) {
    writeLog('WARN', 'localdb', 'Beispiel-Workspace konnte nicht nachgezogen werden: ' + e.message);
    return false;
  }
}
function readLocalDb() {
  ensureDataDir();
  if (!fs.existsSync(DB_FILE)) { const db = emptyLocalDb(); writeLocalDb(db, 'init'); return db; }
  try {
    const raw = fs.readFileSync(DB_FILE, 'utf8');
    const db = JSON.parse(raw || '{}');
    db.fileFormat = db.fileFormat || 'SoloWorkManagerDatabase';
    db.version = db.version || DB_VERSION;
    db.appVersion = APP_VERSION;
    db.meta = db.meta || { workspaces: [], activeWorkspaceId: null, localSettings: {} };
    if (!Array.isArray(db.meta.workspaces)) db.meta.workspaces = [];
    db.meta.workspaces = db.meta.workspaces.map(normalizeWorkspaceMeta);
    if (!db.meta.localSettings || typeof db.meta.localSettings !== 'object') db.meta.localSettings = {};
    db.workspacesData = db.workspacesData || {};
    for (const ws of db.meta.workspaces) {
      if (ws && ws.id && !db.workspacesData[ws.id]) db.workspacesData[ws.id] = emptyWorkspaceData();
    }
    if (ensureIntegratedSampleWorkspace(db)) writeLocalDb(db, 'ensure-example-workspace');
    return db;
  } catch (e) {
    const corrupt = DB_FILE + '.corrupt-' + backupTimestamp();
    try { fs.copyFileSync(DB_FILE, corrupt); } catch (_) {}
    writeLog('ERROR', 'localdb', 'Database file was unreadable; created new empty database', { error: e.message, corruptCopy: corrupt });
    const db = emptyLocalDb(); writeLocalDb(db, 'recover'); return db;
  }
}
function writeLocalDb(db, reason) {
  ensureDataDir();
  db.updatedAt = new Date().toISOString();
  db.appVersion = APP_VERSION;
  const tmp = DB_FILE + '.tmp';
  fs.writeFileSync(tmp, JSON.stringify(db, null, 2), 'utf8');
  fs.renameSync(tmp, DB_FILE);
  if (reason) writeLog('INFO', 'localdb', 'Program-folder database written', { reason });
}
function sendJson(res, status, obj, origin) {
  res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8', 'Access-Control-Allow-Origin': origin, 'Cache-Control': 'no-cache' });
  res.end(JSON.stringify(obj));
}
function readRequestJson(req, maxBytes, cb, onError) {
  let body = '';
  req.on('data', chunk => { body += chunk; if (body.length > (maxBytes || MAX_BACKUP_BYTES)) { onError(new Error('Payload too large')); req.destroy(); } });
  req.on('end', () => { try { cb(JSON.parse(body || '{}')); } catch(e) { onError(e); } });
  req.on('error', () => onError(new Error('Request error')));
}
function safeStoreName(name) { return STORE_NAMES.includes(String(name || '')) ? String(name) : null; }
function workspaceData(db, workspaceId) { const id = String(workspaceId || '').trim(); if (!id) throw new Error('Missing workspaceId'); if (!db.workspacesData[id]) db.workspacesData[id] = emptyWorkspaceData(); return db.workspacesData[id]; }
function getRecordId(rec) { return rec && Object.prototype.hasOwnProperty.call(rec, 'key') ? rec.key : rec && rec.id; }
function localDbSnapshot(db) {
  const meta = db.meta || {};
  const workspaces = Array.isArray(meta.workspaces) ? meta.workspaces : [];
  const activeWorkspaceId = meta.activeWorkspaceId || (workspaces[0] && workspaces[0].id) || null;
  const all = {};
  workspaces.forEach(ws => { if (ws && ws.id) all[ws.id] = db.workspacesData[ws.id] || emptyWorkspaceData(); });
  return { version: 6, exportedAt: new Date().toISOString(), appVersion: APP_VERSION, storage: 'program-folder-database', databaseFile: DB_FILE, workspaces, activeWorkspaceId, localSettings: meta.localSettings || {}, workspacesData: all };
}

// ─── Backup helpers ─────────────────────────────────────────────────────────

const BACKUP_DIR = path.join(USER_DATA_DIR, 'Backups');
const MAX_BACKUP_BYTES = 50 * 1024 * 1024; // 50 MB JSON safety limit
const MAX_AUTO_BACKUPS = 30;

function ensureBackupDir() {
  if (!fs.existsSync(BACKUP_DIR)) fs.mkdirSync(BACKUP_DIR, { recursive: true });
}

function backupTimestamp() {
  return new Date().toISOString().replace(/:/g, '-').replace(/\.\d{3}Z$/, 'Z');
}

function isSafeBackupName(name) {
  return /^solo_workmanager_(auto|manual|pre_restore)_backup_\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}Z\.json$/.test(name || '');
}

function listBackupFiles() {
  ensureBackupDir();
  return fs.readdirSync(BACKUP_DIR)
    .filter(isSafeBackupName)
    .map((name) => {
      const full = path.join(BACKUP_DIR, name);
      const stat = fs.statSync(full);
      return { name, size: stat.size, modifiedAt: stat.mtime.toISOString() };
    })
    .sort((a, b) => b.name.localeCompare(a.name));
}

function rotateAutoBackups() {
  try {
    const autos = listBackupFiles().filter((f) => f.name.startsWith('solo_workmanager_auto_backup_'));
    autos.slice(MAX_AUTO_BACKUPS).forEach((f) => {
      try { fs.unlinkSync(path.join(BACKUP_DIR, f.name)); } catch (e) {}
    });
  } catch (e) {
    writeLog('WARN', 'server', 'Backup rotation failed: ' + e.message);
  }
}

// ─── Logging ────────────────────────────────────────────────────────────────

function ensureLogDir() {
  if (!fs.existsSync(LOG_DIR)) fs.mkdirSync(LOG_DIR, { recursive: true });
}

function getLogPath() {
  const date = new Date().toISOString().slice(0, 10); // YYYY-MM-DD
  const base = path.join(LOG_DIR, `solo-workmanager-${date}.log`);
  // Rotate if current file exceeds MAX_LOG_BYTES
  if (!fs.existsSync(base) || fs.statSync(base).size < MAX_LOG_BYTES) return base;
  for (let i = 1; i < 999; i++) {
    const rotated = path.join(LOG_DIR, `solo-workmanager-${date}-${i}.log`);
    if (!fs.existsSync(rotated) || fs.statSync(rotated).size < MAX_LOG_BYTES) return rotated;
  }
  return base; // fallback (shouldn't happen)
}

function writeLog(level, source, message, extra) {
  try {
    ensureLogDir();
    const entry = {
      ts:      new Date().toISOString(),
      level:   level,
      source:  source,
      message: message,
    };
    if (extra && Object.keys(extra).length) entry.data = extra;
    fs.appendFileSync(getLogPath(), JSON.stringify(entry) + '\n', 'utf8');
  } catch (e) {
    // Never let logging crash the server
  }
}


function listRecentLogFiles(limit) {
  ensureLogDir();
  try {
    return fs.readdirSync(LOG_DIR)
      .filter((name) => /^solo-workmanager-\d{4}-\d{2}-\d{2}(?:-\d+)?\.log$/.test(name))
      .map((name) => {
        const full = path.join(LOG_DIR, name);
        const stat = fs.statSync(full);
        return { name, full, mtime: stat.mtimeMs, size: stat.size };
      })
      .sort((a, b) => b.mtime - a.mtime)
      .slice(0, limit || 3);
  } catch (e) {
    return [];
  }
}

function readTailLines(file, maxLines) {
  try {
    const raw = fs.readFileSync(file, 'utf8');
    const parts = raw.split(/\r?\n/).filter(Boolean);
    return parts.slice(-(maxLines || 300));
  } catch (e) {
    return [`[Log konnte nicht gelesen werden: ${e.message}]`];
  }
}

function buildLogBundleText() {
  const files = listRecentLogFiles(3);
  const lines = [];
  lines.push('S.O.L.O. WorkManager Log-Bundle');
  lines.push('Erstellt: ' + new Date().toISOString());
  lines.push('Enthaelt die letzten Logzeilen aus bis zu 3 aktuellen Logdateien.');
  lines.push('');
  if (!files.length) lines.push('(Keine Logdateien vorhanden)');
  for (const f of files) {
    lines.push('===== ' + f.name + ' =====');
    lines.push(...readTailLines(f.full, 400));
    lines.push('');
  }
  return lines.join('\n');
}

// ─── MIME & shutdown state ───────────────────────────────────────────────────

const MIME_TYPES = {
  '.html': 'text/html; charset=utf-8',
  '.js':   'application/javascript; charset=utf-8',
  '.css':  'text/css; charset=utf-8',
  '.json': 'application/json',
  '.png':  'image/png',
  '.ico':  'image/x-icon',
  '.svg':  'image/svg+xml',
  '.woff2':'font/woff2',
  '.woff': 'font/woff',
};

const SECURITY_HEADERS = {
  'Content-Security-Policy': "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline' data:; font-src 'self' data:; img-src 'self' data: blob:; connect-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'",
  'X-Content-Type-Options': 'nosniff',
  'Referrer-Policy': 'no-referrer'
};

let shutdownTimer = null;
const activeClients = new Map();
const SERVER_STARTED_AT = new Date().toISOString();
let lastClientSeenAt = Date.now();
let lastAnyRequestAt = Date.now();

function normalizeClientId(value) {
  const id = String(value || '').trim();
  return /^[A-Za-z0-9_.:-]{8,80}$/.test(id) ? id : null;
}

function noteRequestActivity(reason) {
  const now = Date.now();
  lastAnyRequestAt = now;
  lastClientSeenAt = now;
  if (shutdownTimer) {
    clearTimeout(shutdownTimer);
    shutdownTimer = null;
    writeLog('INFO', 'server', 'Pending shutdown canceled by HTTP activity', { reason: reason || 'http-request' });
  }
}

function purgeExpiredClients() {
  const now = Date.now();
  for (const [id, client] of activeClients.entries()) {
    if (now - client.lastSeen > CLIENT_TIMEOUT_MS) {
      activeClients.delete(id);
      writeLog('INFO', 'server', 'Client session expired after long timeout', { clientId: id, ageMs: now - client.lastSeen });
    }
  }
}

function markClientActive(clientId, reason) {
  const id = normalizeClientId(clientId) || ('pw-legacy-' + Date.now().toString(36));
  const now = Date.now();
  activeClients.set(id, { lastSeen: now, reason: reason || 'active' });
  lastClientSeenAt = now;
  lastAnyRequestAt = now;
  if (shutdownTimer) {
    clearTimeout(shutdownTimer);
    shutdownTimer = null;
    writeLog('INFO', 'server', 'Pending shutdown canceled by active client', { clientId: id, reason: reason || 'active' });
  }
  return id;
}

function unregisterClient(clientId) {
  const id = normalizeClientId(clientId);
  if (id && activeClients.has(id)) {
    activeClients.delete(id);
    writeLog('INFO', 'server', 'Client session unregistered', { clientId: id, remainingClients: activeClients.size });
  } else {
    writeLog('INFO', 'server', 'Client unregister ignored because session was not active', { clientId: id || '(invalid)' });
  }
  scheduleIdleShutdown('client-unregister');
}

function scheduleIdleShutdown(reason) {
  purgeExpiredClients();
  if (activeClients.size > 0) {
    if (shutdownTimer) {
      clearTimeout(shutdownTimer);
      shutdownTimer = null;
    }
    return;
  }
  if (shutdownTimer) clearTimeout(shutdownTimer);
  shutdownTimer = setTimeout(() => {
    purgeExpiredClients();
    const idleFor = Date.now() - Math.max(lastClientSeenAt, lastAnyRequestAt);
    if (activeClients.size > 0) {
      writeLog('INFO', 'server', 'Shutdown aborted: active client present', { clients: activeClients.size });
      return;
    }
    if (idleFor < IDLE_SHUTDOWN_DELAY_MS) {
      scheduleIdleShutdown('recent-activity-grace');
      return;
    }
    writeLog('INFO', 'server', 'No active browser sessions — server shutting down', { reason: reason || 'idle', idleForMs: idleFor });
    console.log('[S.O.L.O. WorkManager] Keine aktive Browser-Sitzung mehr — Server beendet sich.');
    process.exit(0);
  }, IDLE_SHUTDOWN_DELAY_MS);
}

// Startup grace: if the launcher started the server but no browser ever registers,
// exit later so the process does not remain orphaned forever.
shutdownTimer = setTimeout(() => scheduleIdleShutdown('startup-grace-expired'), STARTUP_GRACE_MS);

setInterval(() => {
  purgeExpiredClients();
  if (activeClients.size === 0 && !shutdownTimer) {
    const idleFor = Date.now() - Math.max(lastClientSeenAt, lastAnyRequestAt);
    if (idleFor >= IDLE_SHUTDOWN_DELAY_MS) scheduleIdleShutdown('periodic-idle-check');
  }
}, 30000).unref();

// ─── HTTP Server ─────────────────────────────────────────────────────────────

const server = http.createServer((req, res) => {
  const origin = `http://localhost:${PORT}`;
  for (const [key, value] of Object.entries(SECURITY_HEADERS)) res.setHeader(key, value);

  // CORS pre-flight (for /log POST from browser)
  if (req.method === 'OPTIONS') {
    res.writeHead(204, {
      'Access-Control-Allow-Origin':  origin,
      'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
      'Access-Control-Allow-Headers': 'Content-Type',
    });
    res.end();
    return;
  }

  // ── /health (launcher/server identity check) ──────────────────────
  if (req.url === '/health') {
    const indexPath = path.join(DIST_DIR, 'index.html');
    const distOk = fs.existsSync(indexPath);
    res.writeHead(distOk ? 200 : 503, {
      'Content-Type': 'application/json',
      'Access-Control-Allow-Origin': origin,
      'Cache-Control': 'no-cache',
    });
    res.end(JSON.stringify({
      ok: distOk,
      app: 'SOLO_WORKMANAGER',
      version: APP_VERSION,
      pid: process.pid,
      port: PORT,
      appDir: PROGRAM_DIR,
      appRoot: APP_ROOT,
      userDataDir: USER_DATA_DIR,
      dist: DIST_DIR,
      dbFile: DB_FILE,
      startedAt: SERVER_STARTED_AT,
      uptimeSec: Math.round(process.uptime()),
      activeClients: activeClients.size,
      distOk
    }));
    return;
  }

  // Any normal HTTP/API request proves that the loaded app is currently in use.
  // This does not create a persistent pseudo-client, but it cancels pending shutdowns.
  if (req.method !== 'OPTIONS' && req.url !== '/shutdown') {
    noteRequestActivity('http-request:' + (req.url.split('?')[0] || '/'));
  }

  // ── /ping (legacy heartbeat compatibility) ───────────────────────
  if (req.url === '/ping') {
    noteRequestActivity('legacy-ping');
    res.writeHead(200, {
      'Content-Type': 'application/json',
      'Access-Control-Allow-Origin': origin,
      'Cache-Control': 'no-cache',
    });
    res.end(JSON.stringify({ ok: true, activeClients: activeClients.size }));
    return;
  }

  // ── Client lifecycle API ─────────────────────────────────────────────────
  if ((req.url.startsWith('/client/register') || req.url.startsWith('/client/heartbeat')) && req.method === 'POST') {
    const headers = { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': origin, 'Cache-Control': 'no-cache' };
    let body = '';
    req.on('data', chunk => { body += chunk; if (body.length > 16000) body = body.slice(0, 16000); });
    req.on('end', () => {
      try {
        let request = {};
        try { request = JSON.parse(body || '{}'); } catch (e) {}
        const id = markClientActive(request.clientId, req.url.includes('register') ? 'register' : 'heartbeat');
        res.writeHead(200, headers);
        res.end(JSON.stringify({ ok: true, clientId: id, activeClients: activeClients.size }));
      } catch (e) {
        res.writeHead(400, headers);
        res.end(JSON.stringify({ ok: false, error: e.message }));
      }
    });
    req.on('error', () => { try { res.writeHead(400, headers); res.end(JSON.stringify({ ok: false, error: 'Request error' })); } catch(e) {} });
    return;
  }

  if (req.url.startsWith('/client/unregister') && req.method === 'POST') {
    const headers = { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': origin, 'Cache-Control': 'no-cache' };
    let body = '';
    req.on('data', chunk => { body += chunk; if (body.length > 16000) body = body.slice(0, 16000); });
    req.on('end', () => {
      let request = {};
      try { request = JSON.parse(body || '{}'); } catch (e) {}
      unregisterClient(request.clientId);
      res.writeHead(200, headers);
      res.end(JSON.stringify({ ok: true, activeClients: activeClients.size }));
    });
    req.on('error', () => { try { res.writeHead(400, headers); res.end(JSON.stringify({ ok: false, error: 'Request error' })); } catch(e) {} });
    return;
  }

  // ── /shutdown (legacy compatibility; now delayed and abortable) ───────────
  if (req.url === '/shutdown') {
    res.writeHead(200, {
      'Content-Type': 'application/json',
      'Access-Control-Allow-Origin': origin,
      'Cache-Control': 'no-cache',
    });
    res.end(JSON.stringify({ ok: true, activeClients: activeClients.size }));
    writeLog('INFO', 'server', 'Legacy shutdown requested — using idle-session delay', { activeClients: activeClients.size });
    scheduleIdleShutdown('legacy-shutdown');
    return;
  }

  // ── /pick-file and /pick-folder intentionally disabled for hardened offline release ──
  if ((req.url.startsWith('/pick-file') || req.url.startsWith('/pick-folder')) && req.method === 'GET') {
    const headers = { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': origin, 'Cache-Control': 'no-cache' };
    res.writeHead(200, headers);
    res.end(JSON.stringify({ ok: false, path: '', manual: true, message: 'Native file dialogs are disabled in the hardened offline release. Please enter the local path manually.' }));
    return;
  }

  // ── /open-path (GET — open local file or folder in OS explorer) ─────────
  if (req.url.startsWith('/open-path') && req.method === 'GET') {
    const qs = new URL(req.url, `http://localhost:${PORT}`).searchParams;
    const targetPath = qs.get('p') || '';
    const headers = {
      'Content-Type': 'application/json',
      'Access-Control-Allow-Origin': origin,
      'Cache-Control': 'no-cache',
    };
    if (!targetPath) {
      res.writeHead(400, headers);
      res.end(JSON.stringify({ ok: false, error: 'No path provided' }));
      return;
    }
    const { spawn } = require('child_process');
    const safe = path.normalize(targetPath);
    try {
      const child = spawn('explorer.exe', [safe], { detached: true, windowsHide: true, stdio: 'ignore' });
      child.unref();
      res.writeHead(200, headers);
      res.end(JSON.stringify({ ok: true }));
    } catch (err) {
      writeLog('WARN', 'server', `open-path failed: ${err.message}`, { path: safe });
      res.writeHead(500, headers);
      res.end(JSON.stringify({ ok: false, error: err.message }));
    }
    return;
  }




  // ── Program-folder database API ──────────────────────────────────────────
  if (req.url === '/api/localdb/meta' && req.method === 'GET') {
    try { const db = readLocalDb(); sendJson(res, 200, { ok: true, dbFile: DB_FILE, meta: db.meta, initialized: (db.meta.workspaces || []).length > 0 }, origin); }
    catch (e) { writeLog('ERROR','localdb','meta failed: '+e.message); sendJson(res, 500, { ok:false, error:e.message }, origin); }
    return;
  }
  if (req.url === '/api/localdb/meta' && req.method === 'POST') {
    readRequestJson(req, MAX_BACKUP_BYTES, (request) => {
      try { const db = readLocalDb(); db.meta = db.meta || {}; if (Array.isArray(request.workspaces)) db.meta.workspaces = request.workspaces.map(normalizeWorkspaceMeta); if ('activeWorkspaceId' in request) db.meta.activeWorkspaceId = request.activeWorkspaceId || null; if (request.localSettings && typeof request.localSettings === 'object') db.meta.localSettings = { ...(db.meta.localSettings || {}), ...request.localSettings }; (db.meta.workspaces || []).forEach(ws => { if (ws && ws.id && !db.workspacesData[ws.id]) db.workspacesData[ws.id] = emptyWorkspaceData(); }); writeLocalDb(db, 'meta-update'); sendJson(res, 200, { ok:true, dbFile: DB_FILE, meta: db.meta }, origin); }
      catch(e) { writeLog('ERROR','localdb','meta update failed: '+e.message); sendJson(res, 400, { ok:false, error:e.message }, origin); }
    }, (e) => sendJson(res, 400, { ok:false, error:e.message }, origin));
    return;
  }
  if (req.url === '/api/localdb/snapshot' && req.method === 'GET') {
    try { const db = readLocalDb(); sendJson(res, 200, { ok: true, dbFile: DB_FILE, snapshot: localDbSnapshot(db) }, origin); }
    catch (e) { writeLog('ERROR','localdb','snapshot failed: '+e.message); sendJson(res, 500, { ok:false, error:e.message }, origin); }
    return;
  }
  if (req.url === '/api/localdb/import-snapshot' && req.method === 'POST') {
    readRequestJson(req, MAX_BACKUP_BYTES, (request) => {
      try { const payload = request.snapshot || request.payload || request; const db = readLocalDb(); if (Array.isArray(payload.workspaces)) db.meta.workspaces = payload.workspaces.map(normalizeWorkspaceMeta); if (payload.activeWorkspaceId) db.meta.activeWorkspaceId = payload.activeWorkspaceId; if (payload.localSettings && typeof payload.localSettings === 'object') db.meta.localSettings = { ...(db.meta.localSettings || {}), ...payload.localSettings }; if (payload.workspacesData && typeof payload.workspacesData === 'object') { for (const [wsid, data] of Object.entries(payload.workspacesData)) { db.workspacesData[wsid] = emptyWorkspaceData(); STORE_NAMES.forEach(st => { if (Array.isArray(data[st])) db.workspacesData[wsid][st] = data[st]; }); } } else if (payload.workspace && payload.workspace.id && payload.data) { const id = payload.workspace.id; const list = db.meta.workspaces || []; if (!list.some(w => w.id === id)) list.push(normalizeWorkspaceMeta(payload.workspace)); db.meta.workspaces = list; db.workspacesData[id] = emptyWorkspaceData(); STORE_NAMES.forEach(st => { if (Array.isArray(payload.data[st])) db.workspacesData[id][st] = payload.data[st]; }); } (db.meta.workspaces || []).forEach(ws => { if (ws && ws.id && !db.workspacesData[ws.id]) db.workspacesData[ws.id] = emptyWorkspaceData(); }); writeLocalDb(db, 'snapshot-import'); sendJson(res, 200, { ok:true, dbFile: DB_FILE, meta: db.meta }, origin); }
      catch(e) { writeLog('ERROR','localdb','snapshot import failed: '+e.message); sendJson(res, 400, { ok:false, error:e.message }, origin); }
    }, (e) => sendJson(res, 400, { ok:false, error:e.message }, origin));
    return;
  }
  if (req.url === '/api/localdb/workspace/has-data' && req.method === 'POST') {
    readRequestJson(req, 64000, (request) => { try { const db = readLocalDb(); const data = db.workspacesData[String(request.workspaceId || '')] || {}; const hasData = STORE_NAMES.some(st => Array.isArray(data[st]) && data[st].length > 0); sendJson(res, 200, { ok:true, hasData }, origin); } catch(e) { sendJson(res, 400, { ok:false, error:e.message }, origin); } }, (e) => sendJson(res, 400, { ok:false, error:e.message }, origin));
    return;
  }
  if (req.url === '/api/localdb/workspace/import' && req.method === 'POST') {
    readRequestJson(req, MAX_BACKUP_BYTES, (request) => { try { const db = readLocalDb(); const id = String(request.workspaceId || (request.workspace && request.workspace.id) || '').trim(); if (!id) throw new Error('Missing workspaceId'); if (request.workspace) { const list = db.meta.workspaces || []; const idx = list.findIndex(w => w.id === id); const wsMeta = normalizeWorkspaceMeta(request.workspace); if (idx >= 0) list[idx] = wsMeta; else list.push(wsMeta); db.meta.workspaces = list; if (!db.meta.activeWorkspaceId) db.meta.activeWorkspaceId = id; } db.workspacesData[id] = emptyWorkspaceData(); const data = request.data || {}; STORE_NAMES.forEach(st => { if (Array.isArray(data[st])) db.workspacesData[id][st] = data[st]; }); writeLocalDb(db, 'workspace-import'); sendJson(res, 200, { ok:true, workspaceId:id }, origin); } catch(e) { writeLog('ERROR','localdb','workspace import failed: '+e.message); sendJson(res, 400, { ok:false, error:e.message }, origin); } }, (e) => sendJson(res, 400, { ok:false, error:e.message }, origin));
    return;
  }
  if (req.url === '/api/localdb/store/getAll' && req.method === 'POST') {
    readRequestJson(req, MAX_BACKUP_BYTES, (request) => { try { const st = safeStoreName(request.store); if (!st) throw new Error('Invalid store'); const db = readLocalDb(); const list = workspaceData(db, request.workspaceId)[st] || []; sendJson(res, 200, { ok:true, items:list }, origin); } catch(e) { sendJson(res, 400, { ok:false, error:e.message }, origin); } }, (e) => sendJson(res, 400, { ok:false, error:e.message }, origin));
    return;
  }
  if (req.url === '/api/localdb/store/get' && req.method === 'POST') {
    readRequestJson(req, 64000, (request) => { try { const st = safeStoreName(request.store); if (!st) throw new Error('Invalid store'); const db = readLocalDb(); const list = workspaceData(db, request.workspaceId)[st] || []; const item = list.find(x => getRecordId(x) === request.key) || null; sendJson(res, 200, { ok:true, item }, origin); } catch(e) { sendJson(res, 400, { ok:false, error:e.message }, origin); } }, (e) => sendJson(res, 400, { ok:false, error:e.message }, origin));
    return;
  }
  if (req.url === '/api/localdb/store/put' && req.method === 'POST') {
    readRequestJson(req, MAX_BACKUP_BYTES, (request) => { try { const st = safeStoreName(request.store); if (!st) throw new Error('Invalid store'); const db = readLocalDb(); const data = workspaceData(db, request.workspaceId); const item = request.item; const id = getRecordId(item); if (id === undefined || id === null || id === '') throw new Error('Missing record id/key'); const list = data[st] || []; const idx = list.findIndex(x => getRecordId(x) === id); if (idx >= 0) list[idx] = item; else list.push(item); data[st] = list; writeLocalDb(db, 'store-put:'+st); sendJson(res, 200, { ok:true, key:id }, origin); } catch(e) { writeLog('ERROR','localdb','store put failed: '+e.message); sendJson(res, 400, { ok:false, error:e.message }, origin); } }, (e) => sendJson(res, 400, { ok:false, error:e.message }, origin));
    return;
  }
  if (req.url === '/api/localdb/store/delete' && req.method === 'POST') {
    readRequestJson(req, 64000, (request) => { try { const st = safeStoreName(request.store); if (!st) throw new Error('Invalid store'); const db = readLocalDb(); const data = workspaceData(db, request.workspaceId); data[st] = (data[st] || []).filter(x => getRecordId(x) !== request.key); writeLocalDb(db, 'store-delete:'+st); sendJson(res, 200, { ok:true }, origin); } catch(e) { writeLog('ERROR','localdb','store delete failed: '+e.message); sendJson(res, 400, { ok:false, error:e.message }, origin); } }, (e) => sendJson(res, 400, { ok:false, error:e.message }, origin));
    return;
  }
  if (req.url === '/api/localdb/store/getByIndex' && req.method === 'POST') {
    readRequestJson(req, 64000, (request) => { try { const st = safeStoreName(request.store); if (!st) throw new Error('Invalid store'); const db = readLocalDb(); const list = workspaceData(db, request.workspaceId)[st] || []; const items = list.filter(x => x && x[request.index] === request.value); sendJson(res, 200, { ok:true, items }, origin); } catch(e) { sendJson(res, 400, { ok:false, error:e.message }, origin); } }, (e) => sendJson(res, 400, { ok:false, error:e.message }, origin));
    return;
  }

  // ── /backup/list (GET — list local backup files) ─────────────────────────
  if (req.url === '/backup/list' && req.method === 'GET') {
    const headers = { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': origin, 'Cache-Control': 'no-cache' };
    try {
      const files = listBackupFiles();
      res.writeHead(200, headers);
      res.end(JSON.stringify({ ok: true, backupDir: BACKUP_DIR, files }));
    } catch (e) {
      writeLog('ERROR', 'server', 'backup/list failed: ' + e.message);
      res.writeHead(500, headers);
      res.end(JSON.stringify({ ok: false, error: e.message }));
    }
    return;
  }

  // ── /backup/create (POST — write JSON backup from program-folder database) ─────
  if (req.url === '/backup/create' && req.method === 'POST') {
    const headers = { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': origin, 'Cache-Control': 'no-cache' };
    let body = '';
    req.on('data', chunk => {
      body += chunk;
      if (body.length > 64000) {
        writeLog('WARN', 'server', 'backup/create request metadata too large');
        res.writeHead(413, headers);
        res.end(JSON.stringify({ ok: false, error: 'Backup request too large' }));
        req.destroy();
      }
    });
    req.on('end', () => {
      try {
        let request = {};
        try { request = JSON.parse(body || '{}'); } catch (e) { request = {}; }
        const kindRaw = String(request.kind || 'manual').replace(/-/g, '_');
        const kind = ['auto', 'manual', 'pre_restore'].includes(kindRaw) ? kindRaw : 'manual';

        // Authoritative backup source is the local program-folder database.
        // Legacy browser IndexedDB/localStorage is not used for new backup files.
        const db = readLocalDb();
        const payload = localDbSnapshot(db);

        ensureBackupDir();
        const fileName = `solo_workmanager_${kind}_backup_${backupTimestamp()}.json`;
        const filePath = path.join(BACKUP_DIR, fileName);
        const wrapped = {
          fileFormat: 'SoloWorkManagerBackup',
          backupKind: kind,
          writtenAt: new Date().toISOString(),
          appVersion: APP_VERSION,
          backupSource: 'program-folder-database',
          databaseFile: DB_FILE,
          payload
        };
        fs.writeFileSync(filePath, JSON.stringify(wrapped, null, 2), 'utf8');
        rotateAutoBackups();
        writeLog('INFO', 'server', 'Backup created from program-folder database', { fileName, kind, source: 'program-folder-database' });
        const stat = fs.statSync(filePath);
        res.writeHead(200, headers);
        res.end(JSON.stringify({ ok: true, file: { name: fileName, size: stat.size, modifiedAt: stat.mtime.toISOString() }, backupDir: BACKUP_DIR, source: 'program-folder-database' }));
      } catch (e) {
        writeLog('ERROR', 'server', 'backup/create failed: ' + e.message);
        res.writeHead(400, headers);
        res.end(JSON.stringify({ ok: false, error: e.message }));
      }
    });
    req.on('error', () => { try { res.writeHead(400, headers); res.end(JSON.stringify({ ok: false, error: 'Request error' })); } catch(e) {} });
    return;
  }

  // ── /backup/read?file=... (GET — read only whitelisted backup names) ──────
  if (req.url.startsWith('/backup/read') && req.method === 'GET') {
    const headers = { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': origin, 'Cache-Control': 'no-cache' };
    try {
      const qs = new URL(req.url, `http://localhost:${PORT}`).searchParams;
      const fileName = qs.get('file') || '';
      if (!isSafeBackupName(fileName)) throw new Error('Invalid backup file name');
      const filePath = path.join(BACKUP_DIR, fileName);
      if (!path.resolve(filePath).startsWith(path.resolve(BACKUP_DIR) + path.sep) || !fs.existsSync(filePath)) throw new Error('Backup not found');
      res.writeHead(200, headers);
      res.end(fs.readFileSync(filePath, 'utf8'));
    } catch (e) {
      writeLog('ERROR', 'server', 'backup/read failed: ' + e.message);
      res.writeHead(404, headers);
      res.end(JSON.stringify({ ok: false, error: e.message }));
    }
    return;
  }

  // ── /logs/download (GET — log bundle for bug-report EML attachments) ─────
  if (req.url === '/logs/download' && req.method === 'GET') {
    try {
      const text = buildLogBundleText();
      const fileName = `solo_workmanager_logs_${new Date().toISOString().slice(0, 10)}.txt`;
      res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8', 'Cache-Control': 'no-cache' });
      res.end(JSON.stringify({ ok: true, fileName, text }));
    } catch (e) {
      res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' });
      res.end(JSON.stringify({ ok: false, fileName: 'solo_workmanager_logs.txt', text: 'Log-Bundle konnte nicht erstellt werden: ' + e.message }));
    }
    return;
  }

  // ── /logs (GET — return last N lines of today's log for bug reports) ──────
  if (req.url === '/logs' && req.method === 'GET') {
    try {
      const logPath = getLogPath();
      let lines = [];
      if (fs.existsSync(logPath)) {
        const raw = fs.readFileSync(logPath, 'utf8');
        lines = raw.trim().split('\n').slice(-100);
      }
      res.writeHead(200, {
        'Content-Type': 'application/json',
        'Access-Control-Allow-Origin': origin,
        'Cache-Control': 'no-cache',
      });
      res.end(JSON.stringify({ lines, logPath }));
    } catch (e) {
      res.writeHead(500, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': origin });
      res.end(JSON.stringify({ lines: [], error: e.message }));
    }
    return;
  }

  // ── /log  (POST — client error/event logging) ────────────────────────────
  if (req.url === '/log' && req.method === 'POST') {
    let body = '';
    req.on('data', chunk => { body += chunk; if (body.length > 64000) body = body.slice(0, 64000); });
    req.on('end', () => {
      try {
        const entry = JSON.parse(body);
        writeLog(
          entry.level   || 'ERROR',
          entry.source  || 'client',
          entry.message || '(no message)',
          entry.data    || {}
        );
      } catch (e) { /* ignore malformed entries */ }
      res.writeHead(200, {
        'Content-Type': 'text/plain',
        'Access-Control-Allow-Origin': origin,
      });
      res.end('ok');
    });
    req.on('error', () => { res.writeHead(400); res.end('err'); });
    return;
  }

  // ── Static file serving ───────────────────────────────────────────────────
  res.setHeader('Access-Control-Allow-Origin', origin);
  res.setHeader('Service-Worker-Allowed', '/');

  let urlPath = req.url.split('?')[0];
  if (urlPath === '/') urlPath = '/index.html';

  const filePath = path.join(DIST_DIR, urlPath);
  const ext      = path.extname(filePath).toLowerCase();
  const mime     = MIME_TYPES[ext] || 'application/octet-stream';

  const cacheHeaders = ext === '.html'
    ? { 'Cache-Control': 'no-cache, no-store, must-revalidate', 'Pragma': 'no-cache', 'Expires': '0' }
    : { 'Cache-Control': 'no-cache' };

  fs.readFile(filePath, (err, data) => {
    if (err) {
      // SPA fallback → return index.html for unknown routes
      fs.readFile(path.join(DIST_DIR, 'index.html'), (err2, indexData) => {
        if (err2) {
          writeLog('ERROR', 'server', `Static index missing; cannot serve app: ${urlPath}`, { dist: DIST_DIR, index: path.join(DIST_DIR, 'index.html') });
          res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' }); res.end('S.O.L.O. WorkManager konnte die App-Dateien nicht finden. Bitte neu entpacken und über START - S.O.L.O. WorkManager.bat starten.'); return;
        }
        res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', ...cacheHeaders });
        res.end(indexData);
      });
      return;
    }
    res.writeHead(200, { 'Content-Type': mime, ...cacheHeaders });
    res.end(data);
  });
});

// ─── Server startup ───────────────────────────────────────────────────────────

server.listen(PORT, 'localhost', () => {
  writeLog('INFO', 'server', `Server started`, { port: PORT, dist: DIST_DIR, pid: process.pid });
  console.log(`[S.O.L.O. WorkManager] Server läuft auf http://localhost:${PORT}`);
  console.log(`[S.O.L.O. WorkManager] Beendet sich nach dem Schließen der letzten Browser-Sitzung mit Sicherheitswartezeit.`);
  console.log(`[S.O.L.O. WorkManager] Daten: ${USER_DATA_DIR}`);
  console.log(`[S.O.L.O. WorkManager] Logs: ${LOG_DIR}`);
});

server.on('error', (err) => {
  writeLog('ERROR', 'server', `Server error: ${err.code || err.message}`, { code: err.code });
  if (err.code === 'EADDRINUSE') {
    console.error(`[S.O.L.O. WorkManager] Port ${PORT} belegt.`);
  } else {
    console.error('Server Fehler:', err);
  }
  process.exit(1);
});

// ─── Process-level error safety ───────────────────────────────────────────────

process.on('uncaughtException', (err) => {
  writeLog('FATAL', 'server', `Uncaught exception: ${err.message}`, { stack: err.stack });
  console.error('[S.O.L.O. WorkManager] Unbehandelter Fehler:', err);
  process.exit(1);
});

process.on('unhandledRejection', (reason) => {
  const msg = reason instanceof Error ? reason.message : String(reason);
  const stk = reason instanceof Error ? reason.stack  : null;
  writeLog('ERROR', 'server', `Unhandled promise rejection: ${msg}`, stk ? { stack: stk } : {});
});

process.on('exit', (code) => {
  writeLog('INFO', 'server', `Process exit`, { code });
});
