"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const express_1 = require("express");
const config_1 = require("../config");
const sync_config_1 = require("../sync-config");
const sync_scheduler_1 = require("../sync-scheduler");
const sync_service_1 = require("../sync-service");
const router = (0, express_1.Router)();
const VALID_DIRECTIONS = ['push', 'pull', 'bidirectional'];
const VALID_AUTH = ['key', 'password'];
function requireUser(req, res) {
    const u = req.user?.username;
    if (!u) {
        res.status(401).json({ error: 'Unauthenticated' });
        return null;
    }
    return u;
}
// GET /api/sync/config  → current user's config (password redacted)
router.get('/config', (req, res) => {
    const user = requireUser(req, res);
    if (!user)
        return;
    res.json((0, sync_config_1.publicConfig)((0, sync_config_1.getSyncConfig)(user)));
});
// PUT /api/sync/config  body: Partial<SyncConfig> with plain `password` for pw auth
router.put('/config', (req, res) => {
    const user = requireUser(req, res);
    if (!user)
        return;
    const body = (req.body ?? {});
    const existing = (0, sync_config_1.getSyncConfig)(user);
    const next = { ...existing };
    if (typeof body.host === 'string')
        next.host = body.host.trim();
    if (typeof body.port === 'number' && body.port > 0 && body.port < 65536)
        next.port = body.port;
    if (typeof body.user === 'string')
        next.user = body.user.trim();
    if (body.authMethod && VALID_AUTH.includes(body.authMethod))
        next.authMethod = body.authMethod;
    if (typeof body.keyPath === 'string') {
        const candidate = body.keyPath.trim();
        if (candidate && !(0, sync_config_1.isValidKeyPath)(candidate)) {
            res.status(400).json({ error: 'keyPath 不能包含空格、引号、反斜杠、null 字节，或以 `-` 开头（防止 ssh 参数注入）' });
            return;
        }
        next.keyPath = candidate || undefined;
    }
    if (typeof body.remoteRoot === 'string') {
        const rr = body.remoteRoot.trim().replace(/\/+$/, '');
        // Remote root should be an absolute path; relative would be interpreted
        // relative to the ssh user's home, which is easy to misconfigure.
        if (rr && !rr.startsWith('/')) {
            res.status(400).json({ error: 'remoteRoot 必须是绝对路径（/ 开头）' });
            return;
        }
        next.remoteRoot = rr;
    }
    if (body.direction && VALID_DIRECTIONS.includes(body.direction))
        next.direction = body.direction;
    if (Array.isArray(body.defaultExcludes)) {
        next.defaultExcludes = body.defaultExcludes
            .filter((v) => typeof v === 'string')
            .map((s) => s.trim())
            .filter(Boolean)
            .slice(0, 200); // prevent DoS via 10K-item excludes
    }
    if (body.schedule && typeof body.schedule === 'object') {
        const sch = body.schedule;
        const cron = typeof sch.cron === 'string' && sch.cron.trim() ? sch.cron.trim() : existing.schedule.cron;
        // Validate cron now so silent "enabled but never fires" doesn't happen.
        const cronErr = (0, sync_scheduler_1.validateCron)(cron);
        if (cronErr) {
            res.status(400).json({ error: `cron 表达式无效: ${cronErr}` });
            return;
        }
        next.schedule = { enabled: !!sch.enabled, cron };
    }
    if (body.projectExcludes && typeof body.projectExcludes === 'object') {
        const pe = {};
        for (const [k, v] of Object.entries(body.projectExcludes)) {
            if (Array.isArray(v)) {
                pe[k] = v
                    .filter((x) => typeof x === 'string')
                    .map((s) => s.trim())
                    .filter(Boolean)
                    .slice(0, 200);
            }
        }
        next.projectExcludes = pe;
    }
    // Password: only update if explicitly provided
    if (typeof body.password === 'string') {
        if (body.password === '') {
            next.passwordEnc = undefined;
            next.passwordFp = undefined;
        }
        else {
            const { enc, fp } = (0, sync_config_1.encryptPassword)(body.password);
            next.passwordEnc = enc;
            next.passwordFp = fp;
        }
    }
    (0, sync_config_1.setSyncConfig)(next);
    res.json((0, sync_config_1.publicConfig)((0, sync_config_1.getSyncConfig)(user)));
});
// POST /api/sync/reset — revert to defaults (keeps nothing)
router.post('/reset', (req, res) => {
    const user = requireUser(req, res);
    if (!user)
        return;
    (0, sync_config_1.setSyncConfig)({ username: user, ...sync_config_1.DEFAULT_CONFIG });
    res.json((0, sync_config_1.publicConfig)((0, sync_config_1.getSyncConfig)(user)));
});
// POST /api/sync/test — ssh <host> true
router.post('/test', async (req, res) => {
    const user = requireUser(req, res);
    if (!user)
        return;
    const result = await (0, sync_service_1.testConnection)(user);
    res.json(result);
});
// POST /api/sync/project/:id  — sync one project using configured direction
// Optional body: { direction: 'push'|'pull' } to override for this call.
router.post('/project/:id', async (req, res) => {
    const user = requireUser(req, res);
    if (!user)
        return;
    const project = (0, config_1.getProject)(req.params.id);
    if (!project) {
        res.status(404).json({ error: 'Project not found' });
        return;
    }
    if (!(0, config_1.isProjectOwner)(project, user)) {
        res.status(403).json({ error: 'Forbidden' });
        return;
    }
    const body = (req.body ?? {});
    const override = body.direction && VALID_DIRECTIONS.includes(body.direction)
        ? body.direction
        : undefined;
    const result = await (0, sync_service_1.syncProject)(user, project.id, project.name, project.folderPath, override);
    res.json(result);
});
// POST /api/sync/all — sync every owned, non-archived project in sequence.
// Sequential (not parallel) because rsync is bandwidth-bound; parallel
// streams just contend for the same pipe.
//
// Cancel behaviour: the bulk-cancel flag is checked before each project and
// between legs; a mid-flight rsync is SIGTERM'd by POST /cancel-all and the
// loop stops on the next iteration. Already-completed results are preserved
// in the response so the caller can see partial progress.
router.post('/all', async (req, res) => {
    const user = requireUser(req, res);
    if (!user)
        return;
    const projects = (0, config_1.getProjects)().filter((p) => !p.archived && (0, config_1.isProjectOwner)(p, user));
    const results = [];
    (0, sync_service_1.clearBulkCancel)(user);
    // Latch the cancel state on exit (before the finally clears the flag);
    // isBulkCancelled(user) would always be false by the time we build the
    // response otherwise.
    let wasCancelled = false;
    try {
        for (const p of projects) {
            if ((0, sync_service_1.isBulkCancelled)(user)) {
                wasCancelled = true;
                results.push({ projectId: p.id, name: p.name, ok: false, reason: 'cancelled', bytes: 0 });
                continue;
            }
            const r = await (0, sync_service_1.syncProject)(user, p.id, p.name, p.folderPath);
            results.push({
                projectId: p.id,
                name: p.name,
                ok: r.ok,
                skipped: r.skipped,
                reason: r.reason,
                bytes: r.bytes,
            });
        }
        if ((0, sync_service_1.isBulkCancelled)(user))
            wasCancelled = true;
    }
    finally {
        (0, sync_service_1.clearBulkCancel)(user);
    }
    res.json({ total: projects.length, results, cancelled: wasCancelled });
});
// POST /api/sync/cancel/:id  — SIGTERM the running rsync for this project
// (owner only). Safe to call when nothing is in flight.
router.post('/cancel/:id', (req, res) => {
    const user = requireUser(req, res);
    if (!user)
        return;
    const project = (0, config_1.getProject)(req.params.id);
    if (!project) {
        res.status(404).json({ error: 'Project not found' });
        return;
    }
    if (!(0, config_1.isProjectOwner)(project, user)) {
        res.status(403).json({ error: 'Forbidden' });
        return;
    }
    const signalled = (0, sync_service_1.cancelSync)(user, project.id);
    res.json({ cancelled: signalled });
});
// POST /api/sync/cancel-all  — cancel every in-flight sync for this user
// AND break any running /all loop at the next iteration. Returns the list of
// projectIds that had a live rsync signalled.
router.post('/cancel-all', (req, res) => {
    const user = requireUser(req, res);
    if (!user)
        return;
    const ids = (0, sync_service_1.cancelAllForUser)(user);
    res.json({ cancelled: ids });
});
// GET /api/sync/status  → { inFlight: string[] }
router.get('/status', (req, res) => {
    const user = requireUser(req, res);
    if (!user)
        return;
    res.json({ inFlight: (0, sync_service_1.listInFlight)(user) });
});
// GET /api/sync/status/:id  → boolean for a single project
router.get('/status/:id', (req, res) => {
    const user = requireUser(req, res);
    if (!user)
        return;
    res.json({ syncing: (0, sync_service_1.isSyncing)(user, req.params.id) });
});
exports.default = router;
//# sourceMappingURL=sync.js.map