<?php
// ═══════════════════════════════════════════════════════════════════════════════════════════════
// PURPOSE
//   Maps each script to the conf subsections that configure it, parses those fields into
//   form definitions, and writes edits back to the conf file. This is what lets the
//   scheduler page edit a script's settings without the user opening master.conf.
//
// DESIGN PRINCIPLES
//   Section headers in the conf are the schema.
//       The map keys off the literal `# ━━━ Name ━━━` and `# ── Name ──` headers already in
//       the conf files. Documentation structure and form structure are the same thing, so a
//       new setting placed under an existing header appears in the UI with no code change.
//
//   Edits are surgical, never a rewrite.
//       Only the changed lines are replaced. Comments, ordering, spacing and every unrelated
//       value survive untouched — these files are hand-maintained and heavily commented, and
//       a regenerating writer would destroy that.
//
//   Structure only; values are not validated.
//       Consistent with conf_upgrade.sh, this reconciles shape and leaves correctness to the
//       consuming script.
//
// OPERATIONAL SAFEGUARDS
//   An unmatched section yields no fields rather than a wrong write.
//       If the named subsection is not found the field list comes back empty and nothing is
//       written. Guessing at a target line in a conf file is how an unrelated setting gets
//       overwritten.
//
//   Writes are confined to the parsed line range.
//       Each field carries the exact line it came from, so a write cannot land outside the
//       subsection it was read from.
//
// EXPORTS
//   vv_conf_has_sections()     does this script have an editable conf section
//   vv_conf_parse_subsection() fields within one named subsection
//   vv_conf_all_groups()       every mapped group
//   vv_conf_fields_for_script() form definition for one script
//   vv_conf_write_changes()    apply edits back to the conf file
//
// CONFIGURATION
//   CONF_DIR   master.conf and host*.conf are the read and write targets
// ═══════════════════════════════════════════════════════════════════════════════════════════════
require_once __DIR__ . '/config.php';

// confform.php — script→conf-section mapping, field parsing, and write-back.

// Map: script relative id → subsection names (must match # ━━━ Name ━━━ or # ── Name ── headers).
const VV_SCRIPT_CONF_SECTIONS = [
    // Orchestrators
    'Orchestrators/array_started.sh'                  => ['Array Start'],
    'Orchestrators/array_stopping.sh'                 => ['Array Stop'],
    'Orchestrators/watchdog_orchestrator.sh'          => ['Watchdog Orchestrator', 'System Watchdog'],
    'Orchestrators/critical_sync_maintenance.sh'      => ['Critical Sync Maintenance', 'Critical Sync Shares'],
    'Orchestrators/intermediate_sync_maintenance.sh'  => ['Intermediate Sync Maintenance', 'Intermediate Sync Shares'],
    'Orchestrators/daily_sync_maintenance.sh'         => ['Daily Sync Maintenance', 'Daily Sync Shares'],
    'Orchestrators/weekly_sync_maintenance.sh'        => ['Weekly Sync Maintenance', 'Weekly Sync Shares'],
    'Orchestrators/monthly_maintenance.sh'            => ['Monthly Maintenance'],
    'Orchestrators/transcode_management.sh'           => ['Transcode Manager', 'Transcode Server Array', 'Transcodes'],
    // Docker Essentials
    'Docker_Essentials/docker_daily_restart.sh'       => ['Docker Daily Restart'],
    'Docker_Essentials/docker_weekly_restart.sh'      => ['Docker Weekly Restart'],
    'Docker_Essentials/docker_network_connect.sh'     => ['Docker Network Connect'],
    'Docker_Essentials/downloaders_reset.sh'          => ['Downloaders Reset', 'Downloaders'],
    // Watchdogs
    'Watchdogs/docker_watchdog.sh'                    => ['Docker Watchdog'],
    'Watchdogs/resource_watchdog.sh'                  => ['Pressure Levels'],
    'Watchdogs/System/network_watchdog.sh'            => ['Network Watchdog'],
    'Plugin/unraid/Watchdogs/System/webgui_watchdog.sh' => ['WebGUI Watchdog'],
    // Media
    'Media/media_cleaner.sh'                          => ['Media Cleaner'],
    'Media/media_shares_permissions.sh'               => ['Media Permissions'],
    // Arrs Stack
    'Arrs_Stack/arrs_failed_stalled_recovery.sh'      => ['Arr Failed/Stalled Recovery'],
    'Arrs_Stack/radarr_cleanup.sh'                    => ['Arr Cleanup'],
    'Arrs_Stack/lidarr_cleanup.sh'                    => ['Arr Cleanup'],
    'Arrs_Stack/sonarr_cleanup.sh'                    => ['Arr Cleanup'],
    // Monitors
    'Monitors/cert_monitor.sh'                        => ['Certificate Monitor'],
    'Monitors/backup_verify.sh'                       => ['Backup Verify'],
    'Monitors/smart_health.sh'                        => ['SMART Health'],
    'Monitors/bandwidth_monitor.sh'                   => ['Bandwidth Monitor'],
    'Monitors/emby_session_report.sh'                 => ['Emby Session Report'],
    'Monitors/zfs_memory_snapshot.sh'                 => ['ZFS Report'],
];

function vv_conf_has_sections(string $id): bool {
    return !empty(VV_SCRIPT_CONF_SECTIONS[$id] ?? []);
}

// Parse fields from a named subsection in raw conf content.
// Headers accepted:  # ━━━ Name ━━━  OR  # ── Name ──  (any mix of ━ ─ chars).
// Returns array of field defs, or null if subsection not found.
function vv_conf_parse_subsection(string $raw, string $subName, string $filename): ?array {
    $lines  = explode("\n", $raw);
    $n      = count($lines);
    $needle = mb_strtolower(trim(preg_replace('/\s+/', ' ', $subName)));
    $start  = -1;

    for ($i = 0; $i < $n; $i++) {
        if (!preg_match('/^#\s*[━─]{2,}\s+([A-Za-z].+?)\s+[━─]{2,}/', $lines[$i], $m)) continue;
        $t = mb_strtolower(trim(preg_replace('/\s+/', ' ', $m[1])));
        if ($t === $needle) { $start = $i + 1; break; }
    }
    if ($start === -1) return null;

    // End at next section/subsection line (3+ consecutive divider chars after #)
    $end = $n;
    for ($i = $start; $i < $n; $i++) {
        if (preg_match('/^#\s*[━─═=]{3,}/', $lines[$i])) { $end = $i; break; }
    }

    return _vv_conf_parse_field_range($lines, $start, $end, $filename) ?: null;
}

// Parse all config fields between two line indices. Shared by vv_conf_parse_subsection()
// (per-script editor) and vv_conf_all_groups() (full settings view).
function _vv_conf_parse_field_range(array $lines, int $start, int $end, string $filename): array {
    $fields      = [];
    $pendingDesc = [];

    for ($i = $start; $i < $end; $i++) {
        $line = rtrim($lines[$i]);

        if ($line === '' || $line === '#') { $pendingDesc = []; continue; }

        // Pure comment line
        if (preg_match('/^#\s*(.*)$/', $line, $cm)) {
            $inner = trim($cm[1]);
            if ($inner !== '' && !preg_match('/^[━─═=\-\s]+$/', $inner)) {
                $pendingDesc[] = $inner;
            }
            continue;
        }

        $desc        = implode(' ', $pendingDesc);
        $pendingDesc = [];

        // declare -A KEY=(
        if (preg_match('/^(\s*)declare\s+-A\s+([A-Z_][A-Z0-9_]*)\s*=\s*\(/', $line, $m)) {
            $indent = $m[1]; $key = $m[2];
            $blockLines = [];
            $j = $i + 1;
            while ($j < $end && !preg_match('/^\s*\)\s*$/', $lines[$j])) {
                $blockLines[] = rtrim($lines[$j]);
                $j++;
            }
            $fields[] = ['key' => $key, 'value' => implode("\n", $blockLines),
                         'type' => 'assoc_array', 'desc' => $desc, 'file' => $filename, 'indent' => $indent];
            $i = $j;
            continue;
        }

        // KEY=( (array)
        if (preg_match('/^(\s*)([A-Z_][A-Z0-9_]*)\s*=\s*\(/', $line, $m)) {
            $indent = $m[1]; $key = $m[2];
            // Single-line: KEY=( ... )
            if (preg_match('/^[^(]*\(([^)]*)\)/', $line, $sm)) {
                $fields[] = ['key' => $key, 'value' => $sm[1],
                             'type' => 'array_single', 'desc' => $desc, 'file' => $filename, 'indent' => $indent];
                continue;
            }
            // Multi-line
            $blockLines = [];
            $j = $i + 1;
            while ($j < $end && !preg_match('/^\s*\)\s*$/', $lines[$j])) {
                $blockLines[] = rtrim($lines[$j]);
                $j++;
            }
            $fields[] = ['key' => $key, 'value' => implode("\n", $blockLines),
                         'type' => 'array', 'desc' => $desc, 'file' => $filename, 'indent' => $indent];
            $i = $j;
            continue;
        }

        // Scalar: KEY="value" or KEY=value
        if (preg_match('/^\s*([A-Z_][A-Z0-9_]*)\s*=\s*"([^"]*)"(?:\s+#\s*(.+))?$/', $line, $m)) {
            $fields[] = ['key' => $m[1], 'value' => $m[2],
                         'type' => 'scalar', 'desc' => ($m[3] ?? '') ?: $desc, 'file' => $filename];
            continue;
        }
        if (preg_match('/^\s*([A-Z_][A-Z0-9_]*)\s*=\s*([^(\n#]*?)(?:\s+#\s*(.+))?$/', $line, $m)) {
            $val = trim($m[2]);
            if ($val === '') continue;
            $fields[] = ['key' => $m[1], 'value' => $val,
                         'type' => 'scalar', 'desc' => ($m[3] ?? '') ?: $desc, 'file' => $filename];
        }
    }

    return $fields;
}

// Return ALL config groups (every named section + its fields) for a conf file.
// Enumerates header lines (# ━━━ Name ━━━ or # ── Name ──); each group runs from its
// header to the next named header so major sections (sandwiched in ===) capture their
// settings too. Empty groups (divider-only headers) are dropped.
function vv_conf_all_groups(string $filename): array {
    $raw = vv_read_conf_raw($filename);
    if ($raw === '') return [];
    $lines = explode("\n", $raw);
    $n     = count($lines);

    $headers = [];
    for ($i = 0; $i < $n; $i++) {
        if (preg_match('/^#\s*[━─]{2,}\s+([A-Za-z].+?)\s+[━─]{2,}/', $lines[$i], $m)) {
            $headers[] = ['name' => trim(preg_replace('/\s+/', ' ', $m[1])), 'line' => $i];
        }
    }

    $groups = [];
    foreach ($headers as $idx => $h) {
        $start  = $h['line'] + 1;
        $end    = $headers[$idx + 1]['line'] ?? $n;
        $fields = _vv_conf_parse_field_range($lines, $start, $end, $filename);
        if ($fields) {
            $groups[] = ['subsection' => $h['name'], 'file' => $filename, 'fields' => $fields];
        }
    }
    return $groups;
}

// Return all conf groups (subsection + fields) for a script on the current host.
function vv_conf_fields_for_script(string $id): array {
    $sectionNames = VV_SCRIPT_CONF_SECTIONS[$id] ?? [];
    if (!$sectionNames) return [];

    $groups = [];
    foreach ($sectionNames as $name) {
        foreach (vv_get_conf_files() as $filename) {
            $fields = vv_conf_parse_subsection(vv_read_conf_raw($filename), $name, $filename);
            if ($fields !== null) {
                $groups[] = ['subsection' => $name, 'file' => $filename, 'fields' => $fields];
            }
        }
    }
    return $groups;
}

// Write a batch of field changes back to their respective conf files.
// Each change: {file, key, value, type}
function vv_conf_write_changes(array $changes): array {
    $byFile = [];
    foreach ($changes as $c) {
        if (!empty($c['file']) && !empty($c['key'])) $byFile[$c['file']][] = $c;
    }

    $results = [];
    foreach ($byFile as $file => $fileChanges) {
        $raw = vv_read_conf_raw($file);
        if ($raw === '') { $results[$file] = false; continue; }

        foreach ($fileChanges as $c) {
            $qKey  = preg_quote($c['key'], '/');
            $value = $c['value'];
            $type  = $c['type'] ?? 'scalar';

            if ($type === 'scalar') {
                $raw = preg_replace_callback(
                    '/^(\s*' . $qKey . '\s*=\s*)("(?:[^"\\\\]|\\\\.)*"|\'(?:[^\'\\\\]|\\\\.)*\'|[^#\n]*?)(\s*(?:#[^\n]*)?)$/m',
                    fn($m) => $m[1] . '"' . str_replace(['"', '\\'], ['\\"', '\\\\'], $value) . '"' . $m[3],
                    $raw
                ) ?? $raw;

            } elseif ($type === 'array_single') {
                $raw = preg_replace_callback(
                    '/^(\s*' . $qKey . '\s*=\s*\()([^)]*)(\)(?:\s*(?:#[^\n]*)?)?)$/m',
                    fn($m) => $m[1] . $value . $m[3],
                    $raw
                ) ?? $raw;

            } elseif ($type === 'array') {
                $raw = preg_replace_callback(
                    '/^(\s*)(' . $qKey . '\s*=\s*\()[^)]*\)/ms',
                    fn($m) => $m[1] . $m[2] . "\n" . $value . "\n" . $m[1] . ")",
                    $raw
                ) ?? $raw;

            } elseif ($type === 'assoc_array') {
                $raw = preg_replace_callback(
                    '/^(\s*)(declare\s+-A\s+' . $qKey . '\s*=\s*\()[^)]*\)/ms',
                    fn($m) => $m[1] . $m[2] . "\n" . $value . "\n" . $m[1] . ")",
                    $raw
                ) ?? $raw;
            }
        }
        // Only the scalar path escapes its value; the array, array_single and assoc_array
        // paths splice the caller's text into the file verbatim, and the type comes from the
        // request. Every script sources these files, so the result is parsed before it is
        // allowed to replace a working conf.
        $results[$file] = vv_conf_syntax_ok($raw) && vv_write_conf_raw($file, $raw);
    }
    return $results;
}

// bash -n against a private temp copy. Returns true when the content parses as a sourceable
// conf, false otherwise — never writes anything itself.
function vv_conf_syntax_ok(string $content): bool {
    $tmp = tempnam(sys_get_temp_dir(), 'vvconf');
    if ($tmp === false) return true;   // cannot check — do not block the write
    file_put_contents($tmp, $content);
    $out = []; $rc = 0;
    exec('bash -n ' . escapeshellarg($tmp) . ' 2>&1', $out, $rc);
    @unlink($tmp);
    return $rc === 0;
}
