<?php
// ═══════════════════════════════════════════════════════════════════════════════════════════════
// PURPOSE
//   Script folder assignments. Saves the grouping of scripts into named folders on the
//   scheduler page, stored in schedule.json under the __folders key.
//
// OPERATIONAL MODEL
//   Whole-map replacement, not a patch. The page sends the complete folder structure and it
//   replaces __folders entirely, so a removed folder disappears by absence. Reconciling
//   individual moves would need a change log the UI does not have and cannot produce from a
//   drag.
//
//   Stored alongside the schedule rather than in a file of its own, under a key prefixed with
//   __ to keep it out of the job namespace — the same convention __rsync_* uses. Everything
//   that iterates the schedule as jobs skips these keys by prefix.
//
// DESIGN PRINCIPLES
//   Presentation only. Folder membership changes nothing about whether or when a script runs;
//   vv_cron_rebuild() never reads __folders, which is why this endpoint does not trigger one.
//
//   Invalid entries are dropped, not rejected.
//       A bad folder name or an unrecognised script id is skipped and the rest of the map is
//       saved. Failing the whole request would lose a full reorganisation over one stale
//       entry — and the entries most likely to be stale are scripts deleted since the page
//       loaded.
//
// OPERATIONAL SAFEGUARDS
//   POST only, checked before the payload is read.
//
//   The payload must decode to an array.
//       is_array() on the decoded JSON — a truncated or non-JSON body is refused outright
//       rather than writing an empty map, which would silently erase every folder.
//
//   Every script id is validated exactly as the run paths validate it.
//       ^[A-Za-z0-9_.\-/]+\.sh$ with an explicit '..' check, applied per entry. These ids are
//       stored and later handed back to the page as job references, so a value that could not
//       be run has no business being persisted next to ones that can.
//
//   Folder names are length-capped and type-coerced.
//       Cast to string, trimmed, empty rejected, and capped at 80 characters — a JSON object
//       with numeric or absurdly long keys cannot bloat schedule.json, which is read on every
//       scheduler page load and every cron rebuild.
//
//   Non-array folder contents are skipped.
//       is_array() per folder before iterating, so a malformed value cannot raise a warning
//       into the JSON response.
//
//   The rest of the schedule is preserved.
//       The file is loaded, one key replaced, and the whole structure written back — job
//       entries and __rsync_* keys are carried through untouched.
//
// REQUEST
//   POST folders=<JSON object: {"<folder name>": ["Category/script.sh", …], …}>
//
// RESPONSE
//   {"ok":true}
//   {"ok":false,"error":"POST only"|"Invalid JSON"|"Write failed"}
//
// DEPENDS ON
//   include/scheduler.php   vv_schedule_load(), vv_schedule_save()
// ═══════════════════════════════════════════════════════════════════════════════════════════════
header('Content-Type: application/json');
require_once dirname(__DIR__) . '/include/config.php';
require_once dirname(__DIR__) . '/include/scheduler.php';

if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
    echo json_encode(['ok' => false, 'error' => 'POST only']);
    exit;
}

$raw     = $_POST['folders'] ?? '';
$decoded = json_decode($raw, true);
if (!is_array($decoded)) {
    echo json_encode(['ok' => false, 'error' => 'Invalid JSON']);
    exit;
}

$clean = [];
foreach ($decoded as $name => $scripts) {
    $name = trim((string)$name);
    if (!$name || strlen($name) > 80) continue;
    if (!is_array($scripts)) continue;
    $cleanScripts = [];
    foreach ($scripts as $s) {
        $s = trim((string)$s);
        if (!$s || str_contains($s, '..') || !preg_match('/^[A-Za-z0-9_.\-\/]+\.sh$/', $s)) continue;
        $cleanScripts[] = $s;
    }
    $clean[$name] = $cleanScripts;
}

$schedule = vv_schedule_load();
$schedule['__folders'] = $clean;
if (!vv_schedule_save($schedule)) {
    echo json_encode(['ok' => false, 'error' => 'Write failed']);
    exit;
}

echo json_encode(['ok' => true]);
