<?php
// ═══════════════════════════════════════════════════════════════════════════════════════════════
// PURPOSE
//   Rewrites one *_SCRIPTS array in master.conf with a new order and a new enabled/disabled
//   state per entry — the drag-to-reorder on the scheduler page's orchestrator view.
//
// OPERATIONAL MODEL
//   Order is behaviour, not presentation. An orchestrator runs its array top to bottom, so
//   moving an entry changes when that script runs relative to the others — which is why this
//   writes to master.conf rather than to a UI preference.
//
//   The array block is located, its entries harvested, and the whole block replaced. Only the
//   lines between the opening and closing parens are touched; everything else in master.conf
//   is written back byte for byte, because the file is hand-maintained and full of comments
//   and grouping no round trip through a parser would preserve.
//
//   Disabled entries stay in the file, commented. Enabled state is expressed by the presence
//   or absence of a leading '# ', the same convention conf_toggle.php uses.
//
// DESIGN PRINCIPLES
//   Original entry text is preserved, not regenerated.
//       Entries are harvested from the existing block keyed by script path, and reused
//       verbatim when the same path appears in the new order. That is what keeps inline flags
//       and arguments — "Media/cleanup.sh --deep" — through a reorder. Only a script that was
//       not previously in the array is written fresh, as a bare quoted path.
//
//   The opening and closing lines are preserved exactly.
//       Both are carried over untouched rather than rebuilt, so a trailing comment on the
//       array declaration survives.
//
//   Absent means removed.
//       A script in the file but not in the submitted order is dropped from the array. The
//       page always submits the complete list, so absence is an instruction, not an omission.
//
// OPERATIONAL SAFEGUARDS
//   POST only, checked before any parameter is read.
//
//   The array name is pattern-matched and only ever used as a needle.
//       ^[A-Z_]+_SCRIPTS$, then preg_quote()d and matched against existing lines. Nothing is
//       opened or executed from it.
//
//   Every entry is validated, and an invalid one fails the request rather than being skipped.
//       ^[A-Za-z0-9_.\-/]+\.sh$ with an explicit '..' check per entry. Skipping would be
//       actively dangerous here: the array is rebuilt from the validated list alone, so a
//       silently dropped entry is a script silently removed from its orchestrator. Validation
//       completes before anything is written.
//
//   A missing array aborts before the write.
//       Both the block start and end must be found, otherwise the endpoint returns a named
//       error and writes nothing. Without that, a typo'd array name would splice a block into
//       an undefined position.
//
//   master.conf is confirmed present and readable before either pass.
//
//   The write is atomic.
//       vv_write_conf_raw() writes .vv.tmp and rename()s. Every script sources master.conf, so
//       a truncated write here would be a system-wide outage rather than a lost reorder.
//
//   The push happens only after a confirmed write, and its result is returned — a partner
//   that did not receive the new order runs these scripts in a different sequence.
//
//   Known limit: block detection counts parens textually.
//       depth is tracked with substr_count over '(' and ')', which does not know about quotes
//       or comments. An entry whose arguments contained an unbalanced paren would end the
//       block early. No current entry does, and the alternative is a bash parser — but a
//       future entry with a paren in its arguments would be the thing that broke this.
//
// REQUEST
//   POST array_name=<NAME>_SCRIPTS
//        scripts=<JSON array of {"id":"Category/name.sh","enabled":bool}>
//
// RESPONSE
//   {"ok":true,"push":[{"host","ok","ready","error"}, …]}
//   {"ok":false,"error":"POST only"|"Invalid array_name"|"Invalid scripts JSON"
//                      |"Invalid script id: …"|"master.conf not found"
//                      |"Could not read master.conf"|"Array … not found in master.conf"
//                      |"Write failed"}
//
// DEPENDS ON
//   include/config.php   CONF_DIR, vv_write_conf_raw(), vv_push_master_conf(),
//                        vv_push_setup_state()
// ═══════════════════════════════════════════════════════════════════════════════════════════════
header('Content-Type: application/json');
require_once dirname(__DIR__) . '/include/config.php';

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

$arrayName = trim($_POST['array_name'] ?? '');
$raw       = $_POST['scripts'] ?? '';
$decoded   = json_decode($raw, true);

if (!$arrayName || !preg_match('/^[A-Z_]+_SCRIPTS$/', $arrayName)) {
    echo json_encode(['ok' => false, 'error' => 'Invalid array_name']);
    exit;
}
if (!is_array($decoded)) {
    echo json_encode(['ok' => false, 'error' => 'Invalid scripts JSON']);
    exit;
}

// Validate every entry before anything is written. A rejected entry cannot be skipped here:
// this endpoint rewrites the array from $order alone, so a silently dropped entry is a script
// silently removed from its orchestrator.
$order = [];
foreach ($decoded as $item) {
    $id      = trim((string)($item['id']      ?? ''));
    $enabled = (bool)($item['enabled'] ?? true);
    if (!$id || str_contains($id, '..') || !preg_match('/^[A-Za-z0-9_.\-\/]+\.sh$/', $id)) {
        echo json_encode(['ok' => false, 'error' => 'Invalid script id: ' . $id]);
        exit;
    }
    $order[] = ['id' => $id, 'enabled' => $enabled];
}

$confPath = CONF_DIR . '/master.conf';
if (!file_exists($confPath)) {
    echo json_encode(['ok' => false, 'error' => 'master.conf not found']);
    exit;
}

$lines = file($confPath, FILE_KEEP_BLANK_LINES);
if (!$lines) {
    echo json_encode(['ok' => false, 'error' => 'Could not read master.conf']);
    exit;
}

// Find the array block and extract original entry lines keyed by script path.
$arrayEsc  = preg_quote($arrayName, '/');
$blockStart = null;
$blockEnd   = null;
$depth      = 0;
$origEntries = []; // path → original trimmed content line (e.g. '"Daily/script.sh --flag"')

foreach ($lines as $i => $line) {
    if ($blockStart === null) {
        if (preg_match('/^\s*' . $arrayEsc . '\s*=\s*\(/', $line)) {
            $blockStart = $i;
            $depth = 1;
        }
        continue;
    }
    $depth += substr_count($line, '(');
    $depth -= substr_count($line, ')');
    if ($depth <= 0) {
        $blockEnd = $i;
        break;
    }
    // Collect entries (enabled and commented)
    if (preg_match('/^\s*(?:#\s*)?"([^"]+)"/', $line, $m)) {
        $parts = preg_split('/\s+/', trim($m[1]));
        $path  = $parts[0] ?? '';
        if (substr($path, -3) === '.sh' && !isset($origEntries[$path])) {
            // Store the full quoted expression (may include flags after the path)
            $origEntries[$path] = '"' . $m[1] . '"';
        }
    }
}

if ($blockStart === null || $blockEnd === null) {
    echo json_encode(['ok' => false, 'error' => "Array $arrayName not found in master.conf"]);
    exit;
}

// Build replacement block lines
$newBlockLines = [];
// Preserve the opening line exactly (e.g. "DAILY_SCRIPTS=(")
$newBlockLines[] = $lines[$blockStart];

foreach ($order as $item) {
    $id      = $item['id'];
    $enabled = $item['enabled'];
    $entry   = $origEntries[$id] ?? '"' . $id . '"';
    $prefix  = $enabled ? '  ' : '  # ';
    $newBlockLines[] = $prefix . $entry . "\n";
}

// Preserve the closing line exactly
$newBlockLines[] = $lines[$blockEnd];

// Replace the original block in $lines
array_splice($lines, $blockStart, $blockEnd - $blockStart + 1, $newBlockLines);

if (!vv_write_conf_raw('master.conf', implode('', $lines))) {
    echo json_encode(['ok' => false, 'error' => 'Write failed']);
    exit;
}

// Reported rather than discarded — a partner that did not receive the new order is a partner
// running these scripts in a different sequence. Mirrors rawconf/confform/movescript.
$push = vv_push_master_conf();
vv_push_setup_state();
echo json_encode(['ok' => true, 'push' => $push]);
