<?php
// ═══════════════════════════════════════════════════════════════════════════════════════════════
// PURPOSE
//   Contextual help for one script. Returns its header block plus any documentation sections
//   about it found in the README and Manual files — the info panel on the scheduler page.
//
// OPERATIONAL MODEL
//   Documentation is assembled at request time, not indexed. Two sources are combined: the
//   script's own header comment, which is authoritative because it lives next to the code,
//   and matching sections from the markdown docs, which give the surrounding context the
//   header deliberately leaves out.
//
//   Four documents are searched per request — the top-level README.md and Manual.md, plus the
//   module-level README-<Dir>.md and Manual-<Dir>.md for the script's own folder. That mirrors
//   how the docs are actually organised: general behaviour at the top, specifics per folder.
//
// DESIGN PRINCIPLES
//   Matches headings by slug, with a first-word fallback.
//       The script name is normalised — underscores and hyphens to spaces, lowercased — and a
//       heading matches if it contains the whole slug or, failing that, the first word when
//       that word is longer than three characters. The length floor is what stops a script
//       beginning with "arr" or "sync" from matching every section in the file.
//
//   Intro sections are never matched.
//       The matcher returns false for the intro, because the opening prose of a README
//       mentions many scripts and would otherwise match nearly all of them.
//
//   Every section carries its source label.
//       The panel shows where each block came from, so a reader can tell the module manual
//       from the top-level README rather than seeing one undifferentiated wall of text.
//
//   Works for documents as well as scripts.
//       A non-.sh id yields no header and only the doc sections, so the same endpoint serves
//       the docs tab's own entries.
//
// OPERATIONAL SAFEGUARDS
//   Read-only. Nothing here writes, executes, or schedules anything.
//
//   Traversal is blocked before any path is composed.
//       An explicit '..' check plus ^[A-Za-z0-9_./\-]+$. The slash must be permitted because
//       ids are Category/name.sh, so the '..' test carries the traversal guarantee on its own.
//
//   The filesystem is only touched for ids that name a script.
//       The header read is guarded by both the .sh suffix test and file_exists(), so a
//       well-formed id for a file that is not there returns an empty header rather than a
//       warning that would leak the absolute path into the JSON body.
//
//   Every document is existence-checked before it is searched, and the module-level path is
//   additionally guarded against a dirname of '.' — an id with no directory component would
//   otherwise compose README-..md and search a file that cannot exist.
//
//   Missing documentation is a normal outcome.
//       No header and no matching sections yields ok:true with empty values. A script nobody
//       has written about yet is not an error, and reporting it as one would put a failure in
//       the panel for most custom scripts.
//
// REQUEST
//   GET ?id=<Category/name.sh>   or any documented id
//
// RESPONSE
//   {"ok":true,"name":"…","header":"…","sections":[{"source":"…","body":"…"}, …]}
//   {"ok":false,"error":"Invalid id"}
//
// DEPENDS ON
//   include/scheduler.php   vv_script_header_clean()
//   include/docs.php        vv_readme_section()   (loaded transitively)
//   SCRIPTS_DIR             README.md, Manual.md, README-<Dir>.md, Manual-<Dir>.md
// ═══════════════════════════════════════════════════════════════════════════════════════════════
header('Content-Type: application/json');
require_once dirname(__DIR__) . '/include/scheduler.php';

$id = trim($_GET['id'] ?? '');
if (!$id || str_contains($id, '..') || !preg_match('/^[a-zA-Z0-9_.\/\-]+$/', $id)) {
    echo json_encode(['ok' => false, 'error' => 'Invalid id']);
    exit;
}

$isScript = str_ends_with($id, '.sh');
$name     = basename($id, $isScript ? '.sh' : '');
$dirName  = basename(dirname($id));   // e.g. "Media", "Rsync", "Orchestrators"
$slug     = strtolower(str_replace(['_', '-'], ' ', $name));
$parts    = explode(' ', $slug);
$first    = $parts[0] ?? '';          // e.g. "radarr" from "radarr cleanup"

// Script header (bash scripts only)
$path   = SCRIPTS_DIR . '/' . $id;
$header = ($isScript && file_exists($path)) ? vv_script_header_clean($path) : '';

// Section matcher: heading contains the full slug OR first meaningful word (>3 chars)
$matcher = function(string $heading, bool $isIntro) use ($slug, $first): bool {
    if ($isIntro) return false;
    $h = strtolower(str_replace(['_', '-'], ' ', $heading));
    return str_contains($h, $slug)
        || (strlen($first) > 3 && str_contains($h, $first));
};

// Search main README/Manual + module-level files for this script's directory
$scriptsDir  = SCRIPTS_DIR;
$searchFiles = [];
foreach (['README', 'Manual'] as $docType) {
    $main = "$scriptsDir/$docType.md";
    if (file_exists($main)) $searchFiles[] = [$docType, $main];

    $mod = "$scriptsDir/$docType-$dirName.md";
    if ($dirName && $dirName !== '.' && file_exists($mod) && $mod !== $main) {
        $searchFiles[] = ["$docType — $dirName", $mod];
    }
}

$sections = [];
foreach ($searchFiles as [$label, $file]) {
    $body = vv_readme_section($file, $matcher);
    if ($body) $sections[] = ['source' => $label, 'body' => $body];
}

echo json_encode([
    'ok'       => true,
    'name'     => $name,
    'header'   => $header,
    'sections' => $sections,
]);
