<?php
// ═══════════════════════════════════════════════════════════════════════════════════════════════
// PURPOSE
//   Schedule writer. Saves one job's enabled state, cron expression and log flag — or a whole
//   set of them in one call — and rebuilds varaverk.cron from the result.
//
// OPERATIONAL MODEL
//   schedule.json is the source of truth; varaverk.cron is generated from it. Nothing edits
//   the crontab directly, which is why the generated file carries a "managed by plugin, do
//   not edit manually" banner. Every save here ends in a vv_cron_rebuild() inside the library,
//   so the two can never drift.
//
//   Two paths, same effect. The single-entry path is what the UI's toggles use; the batch
//   path exists so a bulk edit is one load/write/rebuild cycle rather than N of them. The
//   batch path is currently unreferenced by the UI.
//
//   The two paths differ in how they treat a bad cron, deliberately. A single save rejects it
//   and tells the user; a batch save blanks that one field and continues, because failing an
//   entire bulk edit over one malformed row would lose every other change in it.
//
// DESIGN PRINCIPLES
//   Event triggers are stored here but never reach cron.
//       array_start and array_stop are accepted as cron values and recorded in schedule.json,
//       but vv_cron_rebuild() skips them — they fire from the array event hook instead. One
//       schedule holds both kinds of trigger so the page has a single list to render.
//
//   Enablement is decided here, precedence in the library.
//       A child script whose orchestrator is enabled gets no independent cron line, and that
//       suppression lives in vv_cron_rebuild(). This endpoint records intent; the library
//       resolves what that intent means against the rest of the schedule.
//
// OPERATIONAL SAFEGUARDS
//   The job id and the cron expression are both crontab injection surfaces, and are validated
//   as such.
//       vv_cron_rebuild() interpolates both into a generated crontab line inside double
//       quotes. The id must match ^[A-Za-z0-9_./\-]+\.sh$ with no '..', and the cron
//       expression must consist only of [0-9A-Za-z*,\-/ ] — neither can carry a quote, a
//       shell metacharacter, or a newline. Previously the id was checked only for emptiness,
//       and the field-count pattern used \s, which matches newline: a value of "* * * *\nX"
//       satisfied it and would have appended a second, attacker-chosen line to root's crontab.
//       The character-class check is what carries that guarantee now; the field count is a
//       usability check on top of it.
//
//   Both paths share one validator.
//       vv_sched_id_valid() and vv_sched_cron_valid() are called from the single and batch
//       paths alike, so the bulk path cannot become the loose one — which is exactly how the
//       id check came to be missing from it before.
//
//   POST only, checked before any parameter is read.
//
//   Anything other than the literal "1" is false.
//       Both flags compare strictly against '1', so a missing or malformed parameter disables
//       rather than enables. The single-entry path previously used a (bool) cast, under which
//       the string "false" evaluates to true.
//
//   Malformed batch JSON degrades to an empty set.
//       json_decode with a ?: [] fallback, so a truncated payload writes nothing rather than
//       rebuilding the crontab from garbage.
//
// REQUEST
//   POST id=<Category/name.sh>  enabled=0|1  cron=<5 fields|array_start|array_stop|empty>
//                               log_enabled=0|1
//   POST batch=<JSON array of {id, enabled, cron, log_enabled}>
//
// RESPONSE
//   {"ok":true,"error":null}
//   {"ok":false,"error":"POST only"|"Invalid id"|"Invalid cron expression"
//                      |"Failed to write schedule"}
//
// DEPENDS ON
//   include/scheduler.php   vv_schedule_update(), vv_schedule_update_batch(),
//                           vv_cron_rebuild()
//   varaverk.cron           generated output — never edited directly
// ═══════════════════════════════════════════════════════════════════════════════════════════════
header('Content-Type: application/json');
require_once dirname(__DIR__) . '/include/scheduler.php';

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

// Both values below are interpolated into the generated crontab by vv_cron_rebuild(), so
// neither may contain a quote, a shell metacharacter, or a newline.
function vv_sched_id_valid(string $id): bool {
    return $id !== '' && !str_contains($id, '..') && (bool)preg_match('/^[A-Za-z0-9_.\-\/]+\.sh$/', $id);
}

// Five whitespace-separated fields of cron-safe characters only. \s matches newline, so the
// field-count pattern alone would accept a value carrying a second crontab line.
function vv_sched_cron_valid(string $cron): bool {
    if (in_array($cron, ['array_start', 'array_stop'], true)) return true;
    if (!preg_match('/^[0-9A-Za-z*,\-\/ ]+$/', $cron)) return false;
    return (bool)preg_match('/^(\S+ +){4}\S+$/', $cron);
}

// Batch save — all entries in one load/write/rebuild cycle
if (!empty($_POST['batch'])) {
    $entries = json_decode($_POST['batch'], true) ?: [];
    $clean   = [];
    foreach ($entries as $e) {
        $id   = trim($e['id'] ?? '');
        $cron = trim($e['cron'] ?? '');
        if (!vv_sched_id_valid($id)) continue;
        if ($cron && !vv_sched_cron_valid($cron)) $cron = '';
        $clean[] = [
            'id'          => $id,
            'enabled'     => ($e['enabled'] ?? '0') === '1',
            'cron'        => $cron,
            'log_enabled' => ($e['log_enabled'] ?? '0') === '1',
        ];
    }
    $ok = vv_schedule_update_batch($clean);
    echo json_encode(['ok' => $ok, 'error' => $ok ? null : 'Failed to write schedule']);
    exit;
}

$id          = trim($_POST['id']      ?? '');
$enabled     = ($_POST['enabled']     ?? '0') === '1';
$cron        = trim($_POST['cron']    ?? '');
$log_enabled = ($_POST['log_enabled'] ?? '0') === '1';

if (!vv_sched_id_valid($id)) {
    echo json_encode(['ok' => false, 'error' => 'Invalid id']);
    exit;
}

// 5 cron fields, or a known event trigger, or empty
if ($cron && !vv_sched_cron_valid($cron)) {
    echo json_encode(['ok' => false, 'error' => 'Invalid cron expression']);
    exit;
}

$ok = vv_schedule_update($id, $enabled, $cron, $log_enabled);
echo json_encode(['ok' => $ok, 'error' => $ok ? null : 'Failed to write schedule']);
