// server.js — HPS Vorlagen & Signaturen Web-Editor
// Express backend that proxies the Gitea/Forgejo Contents API.
// The Gitea token stays server-side (never reaches the browser) and CORS
// is avoided because the browser only ever talks to this server.

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

const {
    GITEA_URL,
    GITEA_OWNER,
    GITEA_REPO,
    GITEA_BRANCH = 'main',
    GITEA_TOKEN,
    PORT = 3000,
    COMMIT_AUTHOR_NAME = 'Web-Editor',
    COMMIT_AUTHOR_EMAIL = '',
    BASIC_AUTH_USER,
    BASIC_AUTH_PASS,
} = process.env;

const SHARED_FOLDER = '_gemeinsam';
const USER_FOLDER = '_benutzer';
const CONFIG_FOLDER = '_config';
const CLIENTS_FOLDER = '_clients';
const SIG_FOOTERS = 'signatures/footers';
const SIG_HEADERS = 'signatures/headers';

// Demo mode: serve an in-memory sample repo so the editor can be tried out
// without a real Gitea server. Enable with DEMO=1.
const DEMO = process.env.DEMO === '1' || process.env.DEMO === 'true';

// Local mode: read/write a local checkout of the repo on disk instead of
// talking to Gitea. Useful for inspecting/editing real content offline.
const LOCAL_REPO = process.env.LOCAL_REPO || '';
const LOCAL = !!LOCAL_REPO;
const localRoot = LOCAL ? path.resolve(LOCAL_REPO) : null;

const configured = DEMO || LOCAL || !!(GITEA_URL && GITEA_OWNER && GITEA_REPO && GITEA_TOKEN);
if (!configured) {
    console.warn('[WARN] Gitea-Verbindung unvollständig konfiguriert. ' +
        'Bitte GITEA_URL, GITEA_OWNER, GITEA_REPO und GITEA_TOKEN setzen (siehe .env.example).');
}

// ── Gitea API client ──

const apiBase = () => `${GITEA_URL.replace(/\/$/, '')}/api/v1/repos/${GITEA_OWNER}/${GITEA_REPO}`;

const giteaHeaders = () => ({
    'Authorization': `token ${GITEA_TOKEN}`,
    'Content-Type': 'application/json',
    'Accept': 'application/json',
});

const authorInfo = () => {
    if (!COMMIT_AUTHOR_NAME) return {};
    return {
        author: {
            name: COMMIT_AUTHOR_NAME,
            email: COMMIT_AUTHOR_EMAIL ||
                `${COMMIT_AUTHOR_NAME.toLowerCase().replace(/\s+/g, '.')}@local`,
        },
    };
};

const encodePath = (p) => p.split('/').map(encodeURIComponent).join('/');
const toBase64 = (str) => Buffer.from(str, 'utf-8').toString('base64');
const fromBase64 = (b64) => Buffer.from((b64 || '').replace(/\s/g, ''), 'base64').toString('utf-8');

async function giteaError(method, filepath, res) {
    let detail = '';
    try { const body = await res.json(); detail = body.message || JSON.stringify(body); } catch (_) {}
    const err = new Error(`${method} ${filepath}: ${res.status} ${res.statusText}${detail ? ' — ' + detail : ''}`);
    err.status = res.status;
    throw err;
}

async function getFile(filepath) {
    if (LOCAL) return localGetFile(filepath);
    if (DEMO) return demoGetFile(filepath);
    const url = `${apiBase()}/contents/${encodePath(filepath)}?ref=${encodeURIComponent(GITEA_BRANCH)}`;
    const res = await fetch(url, { headers: giteaHeaders() });
    if (res.status === 404) return null;
    if (!res.ok) await giteaError('GET', filepath, res);
    return res.json();
}

async function listDir(dirpath) {
    if (LOCAL) return localListDir(dirpath);
    if (DEMO) return demoListDir(dirpath);
    const pathPart = dirpath ? `/${encodePath(dirpath)}` : '';
    const url = `${apiBase()}/contents${pathPart}?ref=${encodeURIComponent(GITEA_BRANCH)}`;
    const res = await fetch(url, { headers: giteaHeaders() });
    if (res.status === 404) return [];
    if (!res.ok) await giteaError('LIST', dirpath, res);
    const result = await res.json();
    return Array.isArray(result) ? result : [];
}

async function createFile(filepath, content, message) {
    if (LOCAL) return localWrite(filepath, content);
    if (DEMO) return demoWrite(filepath, content);
    const url = `${apiBase()}/contents/${encodePath(filepath)}`;
    const res = await fetch(url, {
        method: 'POST',
        headers: giteaHeaders(),
        body: JSON.stringify({
            content: toBase64(content),
            message: message || `Add ${filepath}`,
            branch: GITEA_BRANCH,
            ...authorInfo(),
        }),
    });
    if (!res.ok) await giteaError('POST', filepath, res);
    return res.json();
}

async function updateFile(filepath, content, sha, message) {
    if (LOCAL) return localWrite(filepath, content);
    if (DEMO) return demoWrite(filepath, content);
    const url = `${apiBase()}/contents/${encodePath(filepath)}`;
    const res = await fetch(url, {
        method: 'PUT',
        headers: giteaHeaders(),
        body: JSON.stringify({
            content: toBase64(content),
            sha,
            message: message || `Update ${filepath}`,
            branch: GITEA_BRANCH,
            ...authorInfo(),
        }),
    });
    if (!res.ok) await giteaError('PUT', filepath, res);
    return res.json();
}

async function deleteFile(filepath, sha, message) {
    if (LOCAL) { fs.rmSync(localResolve(filepath), { force: true }); return { success: true }; }
    if (DEMO) { delete demoStore[filepath]; return { success: true }; }
    const url = `${apiBase()}/contents/${encodePath(filepath)}`;
    const res = await fetch(url, {
        method: 'DELETE',
        headers: giteaHeaders(),
        body: JSON.stringify({
            sha,
            message: message || `Delete ${filepath}`,
            branch: GITEA_BRANCH,
            ...authorInfo(),
        }),
    });
    if (!res.ok) await giteaError('DELETE', filepath, res);
    return res.json();
}

// Save = create-or-update, server resolves the latest sha to avoid races.
async function saveFile(filepath, content, message) {
    const existing = await getFile(filepath);
    if (existing && typeof existing.content === 'string') {
        const existingContent = fromBase64(existing.content);
        if (existingContent === content) {
            return { unchanged: true, sha: existing.sha };
        }
        const r = await updateFile(filepath, content, existing.sha, message);
        return { sha: r.content?.sha };
    }
    const r = await createFile(filepath, content, message);
    return { sha: r.content?.sha };
}

// ── Local filesystem backend ──
// Reads/writes a local checkout of the repo. Mimics the Gitea response shapes
// so the rest of the server is unchanged. Paths are confined to localRoot.

function localResolve(filepath) {
    const abs = path.resolve(localRoot, filepath);
    if (abs !== localRoot && !abs.startsWith(localRoot + path.sep)) {
        throw Object.assign(new Error('Pfad außerhalb des Repos'), { status: 400 });
    }
    return abs;
}

function localGetFile(filepath) {
    const abs = localResolve(filepath);
    if (!fs.existsSync(abs) || !fs.statSync(abs).isFile()) return null;
    const content = fs.readFileSync(abs, 'utf-8');
    return { path: filepath, name: path.basename(filepath), type: 'file', sha: 'local', content: toBase64(content) };
}

function localListDir(dirpath) {
    const abs = localResolve(dirpath || '.');
    if (!fs.existsSync(abs) || !fs.statSync(abs).isDirectory()) return [];
    return fs.readdirSync(abs, { withFileTypes: true })
        .filter(e => e.name !== '.git')
        .map(e => ({
            name: e.name,
            path: dirpath ? `${dirpath}/${e.name}` : e.name,
            type: e.isDirectory() ? 'dir' : 'file',
            sha: 'local',
        }));
}

function localWrite(filepath, content) {
    const abs = localResolve(filepath);
    fs.mkdirSync(path.dirname(abs), { recursive: true });
    fs.writeFileSync(abs, content, 'utf-8');
    return { content: { sha: 'local', path: filepath } };
}

function localAllFiles() {
    const out = [];
    (function walk(dir) {
        const abs = dir ? localResolve(dir) : localRoot;
        for (const e of fs.readdirSync(abs, { withFileTypes: true })) {
            if (e.name === '.git') continue;
            const rel = dir ? `${dir}/${e.name}` : e.name;
            if (e.isDirectory()) walk(rel);
            else out.push({ path: rel, sha: 'local' });
        }
    })('');
    return out;
}

// ── Demo backend (in-memory) ──
// A flat map of repoPath → raw content, seeded with sample data. The demo
// versions of the primitives mimic the Gitea API response shapes so the rest
// of the server works unchanged.

const demoStore = {};

function demoGetFile(filepath) {
    if (!(filepath in demoStore)) return null;
    return { path: filepath, name: filepath.split('/').pop(), type: 'file', sha: 'demo', content: toBase64(demoStore[filepath]) };
}

function demoListDir(dirpath) {
    const prefix = dirpath ? `${dirpath}/` : '';
    const out = [];
    const seenDirs = new Set();
    for (const p of Object.keys(demoStore)) {
        if (prefix && !p.startsWith(prefix)) continue;
        const rest = p.slice(prefix.length);
        const slash = rest.indexOf('/');
        if (slash === -1) {
            out.push({ name: rest, path: p, type: 'file', sha: 'demo' });
        } else {
            const dir = rest.slice(0, slash);
            if (!seenDirs.has(dir)) {
                seenDirs.add(dir);
                out.push({ name: dir, path: prefix + dir, type: 'dir', sha: 'demo' });
            }
        }
    }
    return out;
}

function demoWrite(filepath, content) {
    demoStore[filepath] = content;
    return { content: { sha: 'demo', path: filepath } };
}

function seedDemo() {
    const sig = (name, role) =>
        `<div style="font-family:Arial,sans-serif;font-size:13px;color:#333;line-height:1.5">
  <strong style="color:#0d3b66">${name}</strong><br>
  <span style="color:#666">${role}</span><br>
  Hotel Park Soltau · Winsener Straße 111 · 29614 Soltau
</div>`;

    Object.assign(demoStore, {
        // Empty department (only a .gitkeep) — shows up with no templates yet.
        'Haustechnik/.gitkeep': '',

        '_config/abteilungen.json': JSON.stringify({
            'info@hotel-park-soltau.de': 'Rezeption',
            'veranstaltung@hotel-park-soltau.de': 'Veranstaltungsbuero',
            'it@hotel-park-soltau.de': 'IT',
            'haustechnik@hotel-park-soltau.de': 'Haustechnik',
        }, null, 2),

        '_gemeinsam/Begruessung.html':
            `<p style="font-family:Arial,sans-serif;font-size:14px;color:#222">Sehr geehrte Damen und Herren,</p>
<p style="font-family:Arial,sans-serif;font-size:14px;color:#222">vielen Dank f&uuml;r Ihre Nachricht an das <strong>Hotel Park Soltau</strong>. Wir freuen uns &uuml;ber Ihr Interesse und melden uns schnellstm&ouml;glich bei Ihnen.</p>`,

        '_gemeinsam/Buchungsbestaetigung.html':
            `<p style="font-family:Arial,sans-serif;font-size:14px;color:#222">Guten Tag,</p>
<p style="font-family:Arial,sans-serif;font-size:14px;color:#222">wir best&auml;tigen Ihnen hiermit Ihre Buchung im Hotel Park Soltau. Bei Fragen stehen wir Ihnen jederzeit gerne zur Verf&uuml;gung.</p>
<ul style="font-family:Arial,sans-serif;font-size:14px;color:#222">
  <li>Anreise: ab 15:00 Uhr</li>
  <li>Abreise: bis 11:00 Uhr</li>
  <li>Inklusive Fr&uuml;hst&uuml;cksbuffet</li>
</ul>`,

        'Rezeption/Anfrage Verfuegbarkeit.html':
            `<p style="font-family:Arial,sans-serif;font-size:14px;color:#222">Guten Tag,</p>
<p style="font-family:Arial,sans-serif;font-size:14px;color:#222">gerne pr&uuml;fen wir die Verf&uuml;gbarkeit f&uuml;r Ihren gew&uuml;nschten Zeitraum. K&ouml;nnten Sie uns bitte folgende Angaben mitteilen?</p>
<ul style="font-family:Arial,sans-serif;font-size:14px;color:#222"><li>An- und Abreisedatum</li><li>Anzahl der Personen</li><li>Zimmerkategorie</li></ul>`,

        'Rezeption/Check-in Informationen.html':
            `<p style="font-family:Arial,sans-serif;font-size:14px;color:#222">Liebe G&auml;ste,</p>
<p style="font-family:Arial,sans-serif;font-size:14px;color:#222">Ihr Check-in ist ab 15:00 Uhr m&ouml;glich. Unsere Rezeption ist rund um die Uhr f&uuml;r Sie besetzt. Kostenfreie Parkpl&auml;tze stehen direkt am Hotel zur Verf&uuml;gung.</p>`,

        'Veranstaltungsbuero/Angebot Tagung.html':
            `<p style="font-family:Arial,sans-serif;font-size:14px;color:#222">Sehr geehrte Damen und Herren,</p>
<p style="font-family:Arial,sans-serif;font-size:14px;color:#222">anbei erhalten Sie unser unverbindliches Angebot f&uuml;r Ihre Tagung. Unsere Veranstaltungsr&auml;ume bieten Platz f&uuml;r bis zu 120 Personen &mdash; inklusive moderner Tagungstechnik.</p>`,

        'IT/Passwort zuruecksetzen.html':
            `<p style="font-family:Arial,sans-serif;font-size:14px;color:#222">Hallo,</p>
<p style="font-family:Arial,sans-serif;font-size:14px;color:#222">Ihr Passwort wurde zur&uuml;ckgesetzt. Bitte melden Sie sich mit dem tempor&auml;ren Kennwort an und vergeben Sie umgehend ein neues.</p>`,

        '_benutzer/info@hotel-park-soltau.de/Persoenliche Notiz.html':
            `<p style="font-family:Arial,sans-serif;font-size:14px;color:#222">Kurzer pers&ouml;nlicher Hinweis &mdash; nur f&uuml;r meinen eigenen Gebrauch sichtbar.</p>`,

        'signatures/footers/_default.html':
            `<table style="font-family:Arial,sans-serif;font-size:12px;color:#888;border-top:2px solid #0d3b66;padding-top:8px;margin-top:12px">
  <tr><td>Hotel Park Soltau GmbH · Gesch&auml;ftsf&uuml;hrer: M. Mustermann · Amtsgericht L&uuml;neburg HRB 12345<br>
  Tel. +49 5191 0000 · <a href="https://hotel-park-soltau.de" style="color:#0d3b66">hotel-park-soltau.de</a></td></tr>
</table>`,

        'signatures/footers/Rezeption.html':
            `<table style="font-family:Arial,sans-serif;font-size:12px;color:#888;border-top:2px solid #0d3b66;padding-top:8px;margin-top:12px">
  <tr><td><strong>Rezeption</strong> &mdash; rund um die Uhr f&uuml;r Sie da · Tel. +49 5191 0000-0<br>
  Hotel Park Soltau · Winsener Stra&szlig;e 111 · 29614 Soltau</td></tr>
</table>`,

        'signatures/headers/_vorlage.html':
            sig('[Vorname Nachname]', '[Position]'),

        'signatures/headers/info@hotel-park-soltau.de.max-mustermann.html':
            sig('Max Mustermann', 'Rezeptionsleitung'),

        'signatures/headers/veranstaltung@hotel-park-soltau.de.anna-beispiel.html':
            sig('Anna Beispiel', 'Veranstaltungsmanagement'),
    });
}

if (DEMO) {
    seedDemo();
    console.log('[DEMO] In-Memory-Demodaten geladen — keine echte Gitea-Verbindung nötig.');
}

// Liefert ALLE Dateien des Repos als flache Liste [{path, sha}].
// Gitea: ein einziger rekursiver git/trees-Aufruf (statt vieler listDir).
async function listAllFiles() {
    if (LOCAL) return localAllFiles();
    if (DEMO) return Object.keys(demoStore).map((p) => ({ path: p, sha: 'demo' }));

    const out = [];
    let page = 1, total = Infinity;
    while (out.length < total) {
        const url = `${apiBase()}/git/trees/${encodeURIComponent(GITEA_BRANCH)}?recursive=true&per_page=1000&page=${page}`;
        const res = await fetch(url, { headers: giteaHeaders() });
        if (res.status === 404) return [];
        if (!res.ok) await giteaError('TREE', '', res);
        const data = await res.json();
        total = data.total_count || 0;
        const blobs = (data.tree || []).filter((e) => e.type === 'blob').map((e) => ({ path: e.path, sha: e.sha }));
        out.push(...blobs);
        if (!data.tree || data.tree.length === 0) break;
        page++;
    }
    return out;
}

// ── Express app ──

const app = express();
app.use(express.json({ limit: '5mb' }));

// Optional basic auth — protects the whole editor when credentials are set.
if (BASIC_AUTH_USER && BASIC_AUTH_PASS) {
    app.use((req, res, next) => {
        const hdr = req.headers.authorization || '';
        const [scheme, encoded] = hdr.split(' ');
        if (scheme === 'Basic' && encoded) {
            const [user, pass] = Buffer.from(encoded, 'base64').toString('utf-8').split(':');
            if (user === BASIC_AUTH_USER && pass === BASIC_AUTH_PASS) return next();
        }
        res.set('WWW-Authenticate', 'Basic realm="HPS Web-Editor"');
        res.status(401).send('Authentifizierung erforderlich');
    });
}

// Async route wrapper → forwards errors to the JSON error handler.
const wrap = (fn) => (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next);

function requireConfigured(_req, res, next) {
    if (!configured) {
        return res.status(503).json({ error: 'Gitea-Verbindung nicht konfiguriert. Bitte Umgebungsvariablen setzen.' });
    }
    next();
}

// Public, non-sensitive config (no token).
app.get('/api/config', (_req, res) => {
    res.json({
        configured,
        demo: DEMO,
        local: LOCAL,
        url: GITEA_URL || (LOCAL ? `lokal: ${localRoot}` : (DEMO ? '(Demo-Modus — keine echten Daten)' : null)),
        owner: GITEA_OWNER || (LOCAL ? 'lokal' : (DEMO ? 'demo' : null)),
        repo: GITEA_REPO || (LOCAL ? path.basename(localRoot) : (DEMO ? 'email-vorlagen' : null)),
        branch: GITEA_BRANCH,
        author: COMMIT_AUTHOR_NAME,
    });
});

app.get('/api/health', wrap(async (_req, res) => {
    if (LOCAL) return res.json({ ok: true, repo: `lokal: ${localRoot}` });
    if (DEMO) return res.json({ ok: true, repo: 'demo/email-vorlagen (Demo-Modus)' });
    if (!configured) return res.status(503).json({ ok: false, error: 'nicht konfiguriert' });
    const r = await fetch(`${apiBase()}`, { headers: giteaHeaders() });
    if (!r.ok) return res.status(502).json({ ok: false, error: `${r.status} ${r.statusText}` });
    const info = await r.json();
    res.json({ ok: true, repo: info.full_name });
}));

app.use('/api', requireConfigured);

// Departments = top-level dirs minus the special folders.
app.get('/api/departments', wrap(async (_req, res) => {
    const entries = await listDir('');
    const departments = entries
        .filter(e => e.type === 'dir'
            && ![SHARED_FOLDER, USER_FOLDER, CONFIG_FOLDER, CLIENTS_FOLDER, 'signatures'].includes(e.name)
            && !e.name.startsWith('.'))
        .map(e => e.name)
        .sort((a, b) => a.localeCompare(b, 'de'));
    res.json({ departments });
}));

app.post('/api/departments', wrap(async (req, res) => {
    const name = (req.body?.name || '').trim();
    if (!name) return res.status(400).json({ error: 'Kein Name angegeben' });
    if (/[\/\\:*?"<>|]/.test(name)) return res.status(400).json({ error: 'Ungültiger Abteilungsname' });
    await createFile(`${name}/.gitkeep`, '', `Abteilung "${name}" angelegt (Web-Editor)`);
    res.json({ success: true, name });
}));

// List .html files in any folder. Folder is validated against known prefixes.
const ALLOWED_LIST_PREFIXES = [SHARED_FOLDER, USER_FOLDER, SIG_FOOTERS, SIG_HEADERS];
app.get('/api/files', wrap(async (req, res) => {
    const folder = String(req.query.folder || '');
    const files = (await listDir(folder))
        .filter(f => f.type === 'file' && f.name.endsWith('.html'))
        .map(f => ({ name: f.name, path: f.path, sha: f.sha }))
        .sort((a, b) => a.name.localeCompare(b.name, 'de'));
    res.json({ folder, files });
}));

// Full inventory in ONE request (recursive tree), strukturiert aus den Pfaden.
app.get('/api/tree', wrap(async (_req, res) => {
    const files = await listAllFiles();
    const special = [SHARED_FOLDER, USER_FOLDER, CONFIG_FOLDER, CLIENTS_FOLDER, 'signatures'];
    const dirOf = (p) => { const i = p.lastIndexOf('/'); return i < 0 ? '' : p.slice(0, i); };
    const baseOf = (p) => p.slice(p.lastIndexOf('/') + 1);

    // .html-Dateien direkt in einem bestimmten Ordner
    const htmlIn = (folder) => files
        .filter((f) => dirOf(f.path) === folder && baseOf(f.path).endsWith('.html'))
        .map((f) => ({ name: baseOf(f.path), path: f.path, sha: f.sha }))
        .sort((a, b) => a.name.localeCompare(b.name, 'de'));

    // Abteilungen = oberste Ordner außer den Spezialordnern (auch leere, via .gitkeep)
    const deptSet = new Set();
    for (const f of files) {
        const seg = f.path.split('/')[0];
        if (f.path.includes('/') && !special.includes(seg) && !seg.startsWith('.')) deptSet.add(seg);
    }
    const departments = [...deptSet].sort((a, b) => a.localeCompare(b, 'de'));

    const templates = { [SHARED_FOLDER]: htmlIn(SHARED_FOLDER) };
    departments.forEach((d) => { templates[d] = htmlIn(d); });

    const users = {};
    for (const f of files) {
        const parts = f.path.split('/');
        if (parts[0] === USER_FOLDER && parts.length >= 3) users[parts[1]] = users[parts[1]] || [];
    }
    Object.keys(users).forEach((email) => { users[email] = htmlIn(`${USER_FOLDER}/${email}`); });

    res.json({ departments, templates, users, footers: htmlIn(SIG_FOOTERS), headers: htmlIn(SIG_HEADERS) });
}));

// Read a single file. Missing file → exists:false, empty content (editor can create it).
app.get('/api/file', wrap(async (req, res) => {
    const filepath = String(req.query.path || '');
    if (!filepath || filepath.includes('..')) return res.status(400).json({ error: 'Ungültiger Pfad' });
    const data = await getFile(filepath);
    if (!data) return res.json({ path: filepath, content: '', sha: null, exists: false });
    res.json({ path: filepath, content: fromBase64(data.content), sha: data.sha, exists: true });
}));

// Create or update a file.
app.put('/api/file', wrap(async (req, res) => {
    const { path: filepath, content = '', message } = req.body || {};
    if (!filepath || filepath.includes('..')) return res.status(400).json({ error: 'Ungültiger Pfad' });
    if (!filepath.endsWith('.html') && !filepath.endsWith('.json')) {
        return res.status(400).json({ error: 'Nur .html- oder .json-Dateien erlaubt' });
    }
    const result = await saveFile(filepath, content, message);
    res.json({ success: true, ...result });
}));

// Delete a file.
app.delete('/api/file', wrap(async (req, res) => {
    const filepath = String(req.body?.path || '');
    if (!filepath || filepath.includes('..')) return res.status(400).json({ error: 'Ungültiger Pfad' });
    const data = await getFile(filepath);
    if (!data) return res.status(404).json({ error: 'Datei nicht gefunden' });
    await deleteFile(filepath, data.sha, req.body?.message || `${filepath.split('/').pop()} gelöscht (Web-Editor)`);
    res.json({ success: true });
}));

// Client-Status-Übersicht: liest alle _clients/<email>.json und liefert die
// gemeldeten Plugin-/TB-Versionen je User (Basis für den Admin-Tab).
app.get('/api/clients', wrap(async (_req, res) => {
    const entries = await listDir(CLIENTS_FOLDER);
    const clients = [];
    for (const e of entries) {
        if (e.type !== 'file' || !e.name.endsWith('.json')) continue;
        const data = await getFile(e.path);
        if (!data) continue;
        try {
            const c = JSON.parse(fromBase64(data.content));
            clients.push({
                name: c.name || '',
                email: c.email || e.name.replace(/\.json$/, ''),
                department: c.department || '',
                pluginVersion: c.pluginVersion || '',
                tbVersion: c.tbVersion || '',
                lastSeen: c.lastSeen || '',
            });
        } catch (_) { /* kaputte Statusdatei überspringen */ }
    }
    clients.sort((a, b) => (a.name || a.email).localeCompare(b.name || b.email, 'de'));
    res.json({ clients });
}));

// Read/write the email→department mapping.
app.get('/api/abteilungen', wrap(async (_req, res) => {
    const data = await getFile(`${CONFIG_FOLDER}/abteilungen.json`);
    let mapping = {};
    if (data) { try { mapping = JSON.parse(fromBase64(data.content)); } catch (_) {} }
    res.json({ mapping, exists: !!data });
}));

app.put('/api/abteilungen', wrap(async (req, res) => {
    const mapping = req.body?.mapping;
    if (!mapping || typeof mapping !== 'object') return res.status(400).json({ error: 'Ungültiges Mapping' });
    const json = JSON.stringify(mapping, null, 2);
    const result = await saveFile(`${CONFIG_FOLDER}/abteilungen.json`, json, 'abteilungen.json aktualisiert (Web-Editor)');
    res.json({ success: true, ...result });
}));

// Read/write the Schlagwörter (Thunderbird-Tags) config.
app.get('/api/schlagwoerter', wrap(async (_req, res) => {
    const data = await getFile(`${CONFIG_FOLDER}/schlagwoerter.json`);
    let tags = [];
    if (data) { try { tags = JSON.parse(fromBase64(data.content)); } catch (_) {} }
    if (!Array.isArray(tags)) tags = [];
    res.json({ tags, exists: !!data });
}));

app.put('/api/schlagwoerter', wrap(async (req, res) => {
    const tags = req.body?.tags;
    if (!Array.isArray(tags)) return res.status(400).json({ error: 'Ungültige Schlagwörter' });
    const json = JSON.stringify(tags, null, 2);
    const result = await saveFile(`${CONFIG_FOLDER}/schlagwoerter.json`, json, 'schlagwoerter.json aktualisiert (Web-Editor)');
    res.json({ success: true, ...result });
}));

// Delete a whole department folder (all its files). Destructive – guarded.
app.delete('/api/departments', wrap(async (req, res) => {
    const name = (req.body?.name || '').trim();
    if (!name) return res.status(400).json({ error: 'Kein Name angegeben' });
    if ([SHARED_FOLDER, USER_FOLDER, CONFIG_FOLDER, CLIENTS_FOLDER, 'signatures'].includes(name) || name.includes('/') || name.includes('..')) {
        return res.status(400).json({ error: 'Geschützter oder ungültiger Ordner' });
    }
    const entries = await listDir(name);
    let deleted = 0;
    for (const f of entries) {
        if (f.type !== 'file') continue;
        const fd = await getFile(f.path);
        if (fd) { await deleteFile(f.path, fd.sha, `Abteilung "${name}" gelöscht (Web-Editor)`); deleted++; }
    }
    res.json({ success: true, deleted });
}));

// TinyMCE (selbst gehostet, offline-tauglich) direkt aus node_modules ausliefern.
app.use('/vendor/tinymce', express.static(path.join(__dirname, 'node_modules', 'tinymce')));

// Static frontend.
app.use(express.static(path.join(__dirname, 'public')));

// JSON error handler.
app.use((err, _req, res, _next) => {
    console.error('[ERROR]', err.message);
    res.status(err.status && err.status >= 400 && err.status < 600 ? err.status : 500)
        .json({ error: err.message || 'Interner Fehler' });
});

app.listen(PORT, () => {
    console.log(`HPS Web-Editor läuft auf http://localhost:${PORT}`);
    if (configured) console.log(`→ Repo: ${GITEA_OWNER}/${GITEA_REPO}@${GITEA_BRANCH} (${GITEA_URL})`);
});
