<?php
// ═══════════════════════════════════════════════════════════════════════════════════════════════
// PURPOSE
//   Rsync profile CRUD. Lists, creates, updates and deletes the named transfer profiles that
//   control how a given share is synced — rsync options, bandwidth cap, retry behaviour,
//   container stop/start lists and exclusions.
//
// OPERATIONAL MODEL
//   A profile is not stored as a record. It is one key spread across nine parallel
//   `declare -A PROFILE_*` associative arrays in master.conf — PROFILE_RSYNC_OPTS[name],
//   PROFILE_BW_LIMIT[name], and so on. That layout exists because the shell layer reads each
//   setting independently, and this endpoint's whole job is to present it as a record anyway:
//   read all nine, pivot by profile name, and on save write the same key back into each.
//
//   Every write therefore touches nine array declarations at once, submitted as a single
//   change set so they land together. A profile that existed in some arrays and not others
//   would read back with silently missing settings.
//
// DESIGN PRINCIPLES
//   Create and update are the same operation.
//       Save sets the named key in every array whether or not it was already there. There is
//       no separate create path, and therefore no way for the two to diverge on what a
//       complete profile looks like.
//
//   Delete only touches arrays that actually contain the profile.
//       array_key_exists() is checked per array, and an empty change set is reported as
//       "Profile not found" rather than as a successful no-op.
//
//   Values are quoted only when they need to be.
//       _rp_build_assoc() emits a bare value unless it is empty or contains whitespace or a
//       shell metacharacter. That keeps master.conf readable by hand, which is the reason the
//       whole config layer is bash rather than JSON.
//
//   Missing arrays are skipped, not created.
//       A PROFILE_* declaration absent from master.conf is passed over. This endpoint edits
//       the schema that exists; adding a new setting is a conf template change, not a runtime
//       one.
//
// OPERATIONAL SAFEGUARDS
//   The profile name is constrained to characters that cannot break the array.
//       ^[a-zA-Z0-9_\-]+$ on both save and delete — no spaces, quotes, brackets or shell
//       metacharacters. The name becomes an associative-array subscript, so anything outside
//       that set could terminate the key or the declaration.
//
//   Values are escaped on the way in, and the result is parsed before it lands.
//       Embedded quotes are backslash-escaped by _rp_build_assoc(), and
//       vv_conf_write_changes() then runs `bash -n` over the rewritten file. The escaping
//       handles the expected case; the syntax gate is what catches the unexpected one — and
//       it matters here because assoc_array values are spliced in verbatim rather than
//       through the scalar path's escaping.
//
//   Writes go through the shared conf writer, so they are atomic — tmp + rename — and a
//   script sourcing master.conf mid-save sees the old file or the new one.
//
//   The push happens only after every array write succeeded.
//       Guarded on the combined result, so a partially failed change set is not distributed
//       to partners. Profiles are shared configuration; a partner holding a different
//       definition of a profile would sync the same share differently.
//
//   Save and delete are POST-only; only list is reachable by GET.
//
//   Unknown actions fall through to an explicit error rather than an empty 200.
//
// REQUEST
//   GET|POST ?action=list      all profiles, pivoted into records
//   POST action=save    name=<profile>  rsync_opts, bw_limit, retry_count, sleep,
//                       critical_containers, delayed_containers, container_delay,
//                       exclude_dirs, remote_restart   (all optional, default empty)
//   POST action=delete  name=<profile>
//
// RESPONSE
//   list    {"ok":true,"profiles":{"<name>":{"<field>":"<value>", …}, …}}
//   save    {"ok":bool,"results":{"master.conf":bool}}
//   delete  {"ok":bool}
//   {"ok":false,"error":"Invalid profile name — …"|"No profile arrays found in master.conf"
//                      |"Profile not found"|"Unknown action"}
//
// DEPENDS ON
//   include/confform.php   vv_conf_write_changes()
//   include/config.php     vv_read_conf_raw(), vv_push_master_conf(), vv_push_setup_state()
//   Rsync/rsync.sh         consumer of every PROFILE_* array this writes
// ═══════════════════════════════════════════════════════════════════════════════════════════════
header('Content-Type: application/json');
header('Cache-Control: no-store, no-cache');
require_once dirname(__DIR__) . '/include/config.php';
require_once dirname(__DIR__) . '/include/confform.php';

// All PROFILE_* assoc arrays and their UI field keys
const RP_ARRAYS = [
    'PROFILE_RSYNC_OPTS'                => 'rsync_opts',
    'PROFILE_BW_LIMIT'                  => 'bw_limit',
    'PROFILE_RETRY_COUNT'               => 'retry_count',
    'PROFILE_SLEEP'                     => 'sleep',
    'PROFILE_CRITICAL_CONTAINER_NAMES'  => 'critical_containers',
    'PROFILE_DELAYED_CONTAINERS'        => 'delayed_containers',
    'PROFILE_CONTAINER_DELAY'           => 'container_delay',
    'PROFILE_EXCLUDE_DIRS'              => 'exclude_dirs',
    'PROFILE_REMOTE_RESTART_CONTAINERS' => 'remote_restart',
];

// Parse [key]="value" or [key]=bare entries from an assoc_array body
function _rp_parse_assoc(string $body): array {
    $result = [];
    preg_match_all('/\[([^\]]+)\]\s*=\s*(?:"([^"]*)"|([^\s#\n]*))/', $body, $m, PREG_SET_ORDER);
    foreach ($m as $match) {
        $key          = $match[1];
        $val          = $match[2] !== '' ? $match[2] : ($match[3] ?? '');
        $result[$key] = $val;
    }
    return $result;
}

// Rebuild assoc_array body from entries map
function _rp_build_assoc(array $entries): string {
    $lines = [];
    foreach ($entries as $k => $v) {
        // Quote if empty, has spaces or special shell chars
        if ($v === '' || preg_match('/[\s\$\!\[\]\(\)\|\'`\\\\]/', $v)) {
            $lines[] = '        [' . $k . ']="' . str_replace('"', '\\"', $v) . '"';
        } else {
            $lines[] = '        [' . $k . ']=' . $v;
        }
    }
    return implode("\n", $lines);
}

// Read all PROFILE_* arrays from master.conf → [profile_name => [field => value]]
function _rp_read_all(): array {
    $raw     = vv_read_conf_raw('master.conf');
    $result  = [];
    foreach (RP_ARRAYS as $varName => $fieldKey) {
        if (!preg_match('/declare\s+-A\s+' . preg_quote($varName, '/') . '\s*=\s*\((.*?)\)/s', $raw, $m)) continue;
        foreach (_rp_parse_assoc($m[1]) as $profile => $value) {
            $result[$profile][$fieldKey] = $value;
        }
    }
    return $result;
}

$action = trim($_GET['action'] ?? $_POST['action'] ?? '');

// ── list ──────────────────────────────────────────────────────────────────────
if ($action === 'list') {
    echo json_encode(['ok' => true, 'profiles' => _rp_read_all()]);
    exit;
}

// ── save (create or update) ───────────────────────────────────────────────────
if ($_SERVER['REQUEST_METHOD'] === 'POST' && $action === 'save') {
    $name = trim($_POST['name'] ?? '');
    if (!$name || !preg_match('/^[a-zA-Z0-9_\-]+$/', $name)) {
        echo json_encode(['ok' => false, 'error' => 'Invalid profile name — use letters, numbers, hyphens, underscores']); exit;
    }

    $raw     = vv_read_conf_raw('master.conf');
    $changes = [];

    foreach (RP_ARRAYS as $varName => $fieldKey) {
        $value = trim($_POST[$fieldKey] ?? '');
        // Find and parse current array body
        if (!preg_match('/declare\s+-A\s+' . preg_quote($varName, '/') . '\s*=\s*\((.*?)\)/s', $raw, $m)) continue;
        $entries         = _rp_parse_assoc($m[1]);
        $entries[$name]  = $value;
        $changes[] = [
            'file'  => 'master.conf',
            'key'   => $varName,
            'type'  => 'assoc_array',
            'value' => _rp_build_assoc($entries),
        ];
    }

    if (!$changes) { echo json_encode(['ok' => false, 'error' => 'No profile arrays found in master.conf']); exit; }

    $results = vv_conf_write_changes($changes);
    $ok      = !in_array(false, $results, true);
    if ($ok) { vv_push_master_conf(); vv_push_setup_state(); }
    echo json_encode(['ok' => $ok, 'results' => $results]);
    exit;
}

// ── delete ────────────────────────────────────────────────────────────────────
if ($_SERVER['REQUEST_METHOD'] === 'POST' && $action === 'delete') {
    $name = trim($_POST['name'] ?? '');
    if (!$name || !preg_match('/^[a-zA-Z0-9_\-]+$/', $name)) {
        echo json_encode(['ok' => false, 'error' => 'Invalid profile name']); exit;
    }

    $raw     = vv_read_conf_raw('master.conf');
    $changes = [];

    foreach (RP_ARRAYS as $varName => $fieldKey) {
        if (!preg_match('/declare\s+-A\s+' . preg_quote($varName, '/') . '\s*=\s*\((.*?)\)/s', $raw, $m)) continue;
        $entries = _rp_parse_assoc($m[1]);
        if (!array_key_exists($name, $entries)) continue;
        unset($entries[$name]);
        $changes[] = [
            'file'  => 'master.conf',
            'key'   => $varName,
            'type'  => 'assoc_array',
            'value' => _rp_build_assoc($entries),
        ];
    }

    if (!$changes) { echo json_encode(['ok' => false, 'error' => 'Profile not found']); exit; }

    $results = vv_conf_write_changes($changes);
    $ok      = !in_array(false, $results, true);
    if ($ok) { vv_push_master_conf(); vv_push_setup_state(); }
    echo json_encode(['ok' => $ok]);
    exit;
}

echo json_encode(['ok' => false, 'error' => 'Unknown action']);
