<?php
// ═══════════════════════════════════════════════════════════════════════════════════════════════
// PURPOSE
//   Standalone rsync scheduling. Records a location and cron expression for one rsync tier so
//   that tier can run on its own schedule when its orchestrator is disabled.
//
// OPERATIONAL MODEL
//   Rsync normally runs as a step inside an orchestrator — critical, intermediate, daily,
//   weekly. This exists for the case where someone wants that tier's sync without the rest of
//   the orchestrator's work. vv_cron_rebuild() emits the standalone entry only when the
//   orchestrator is disabled and both a location and a cron are configured, so the two can
//   never both fire: enabling the orchestrator silently takes precedence.
//
//   Stored in schedule.json under __rsync_<FLAG_NAME>, a namespace deliberately outside the
//   job id space. Everything that iterates the schedule as jobs skips keys with that prefix.
//
//   The cron is rebuilt immediately on save, so the change takes effect without waiting for
//   another event to regenerate varaverk.cron.
//
// DESIGN PRINCIPLES
//   Keyed by flag, not by orchestrator.
//       The tier flag is the stable identity — CRITICAL_RSYNC_ENABLED means the same thing
//       regardless of which orchestrator currently carries that tier. orch_id is stored
//       alongside it purely so the rebuild can check whether that orchestrator is enabled.
//
//   Configuration only. Nothing here starts a sync; it records when one should start.
//
//   Empty fields are permitted and mean "not configured".
//       location and cron may both be blank, which is how a standalone entry is cleared —
//       vv_cron_rebuild() requires both to be present before it emits anything.
//
// OPERATIONAL SAFEGUARDS
//   POST only, checked before any parameter is read.
//
//   The flag name is constrained to the rsync namespace.
//       ^[A-Z_]+_RSYNC_ENABLED$ — this endpoint cannot create a schedule key for anything
//       else, so the __rsync_ namespace stays exactly as wide as the tiers it was built for.
//
//   The cron expression is validated as a crontab injection surface.
//       vv_cron_rebuild() interpolates it directly into a generated crontab line. Only
//       [0-9A-Za-z*,\-/ ] is permitted, and then five whitespace-separated fields are
//       required. The character class is what carries the guarantee: the field-count pattern
//       uses \s, which matches newline, so on its own it would accept a value carrying a
//       second, caller-chosen crontab entry. It was previously not validated at all.
//
//   The orchestrator id is validated even though it is only ever used as a lookup key.
//       ^[A-Za-z0-9_.\-/]+\.sh$ with an explicit '..' check, so a value that could not name a
//       real script cannot be stored as though it does.
//
//   The location must be absolute and clean.
//       Leading slash required, '..' rejected, control characters rejected. It reaches the
//       crontab as an escapeshellarg'd --location= token, so validation and escaping are both
//       in place.
//
//   The rest of the schedule is preserved — the file is loaded, one key replaced, and the
//   whole structure written back.
//
//   The cron rebuild only runs after a confirmed write, so a failed save cannot regenerate
//   the crontab from a schedule that was not persisted.
//
// REQUEST
//   POST flag_name=<TIER>_RSYNC_ENABLED  orch_id=<Category/name.sh>
//        location=/absolute/path         cron=<5 fields>
//
// RESPONSE
//   {"ok":true}
//   {"ok":false,"error":"POST only"|"Invalid flag_name"|"Invalid orch_id"|"Invalid location"
//                      |"Invalid cron expression"|"Write failed"}
//
// DEPENDS ON
//   include/scheduler.php   vv_schedule_load(), vv_schedule_save(), vv_cron_rebuild()
//   Rsync/rsync.sh          the script the generated cron entry invokes
// ═══════════════════════════════════════════════════════════════════════════════════════════════
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;
}

$flagName = trim($_POST['flag_name'] ?? '');
$orchId   = trim($_POST['orch_id']   ?? '');
$location = trim($_POST['location']  ?? '');
$cron     = trim($_POST['cron']      ?? '');

if (!$flagName || !preg_match('/^[A-Z_]+_RSYNC_ENABLED$/', $flagName)) {
    echo json_encode(['ok' => false, 'error' => 'Invalid flag_name']);
    exit;
}
if ($orchId && (str_contains($orchId, '..') || !preg_match('/^[A-Za-z0-9_.\-\/]+\.sh$/', $orchId))) {
    echo json_encode(['ok' => false, 'error' => 'Invalid orch_id']);
    exit;
}
if ($location && (!str_starts_with($location, '/') || str_contains($location, '..') || preg_match('/[\x00\n\r]/', $location))) {
    echo json_encode(['ok' => false, 'error' => 'Invalid location']);
    exit;
}
// vv_cron_rebuild() interpolates this straight into a crontab line. Restrict to cron-safe
// characters first — a field-count check alone would accept a value carrying a newline and
// therefore a second, caller-chosen crontab entry.
if ($cron && (!preg_match('/^[0-9A-Za-z*,\-\/ ]+$/', $cron) || !preg_match('/^(\S+ +){4}\S+$/', $cron))) {
    echo json_encode(['ok' => false, 'error' => 'Invalid cron expression']);
    exit;
}

$key      = '__rsync_' . $flagName;
$schedule = vv_schedule_load();
$schedule[$key] = [
    'flag_name' => $flagName,
    'orch_id'   => $orchId,
    'location'  => $location,
    'cron'      => $cron,
];
if (!vv_schedule_save($schedule)) {
    echo json_encode(['ok' => false, 'error' => 'Write failed']);
    exit;
}

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