#!/usr/bin/env php
<?php
// Materialize backup_throttle settings from /docker/whp/settings.json into a
// systemd drop-in at /etc/systemd/system/whp-backup.slice.d/local.conf, then
// reload systemd so the new caps apply to subsequent transient scopes.
//
// Drop-in precedence: values here override whatever is baked into the main
// whp-backup.slice unit by setup-backup-throttle.sh. If settings.json has no
// backup_throttle block, the drop-in is removed (defaults from the main unit
// take over).
//
// Called from:
//   - the server-settings UI when the operator saves throttle values
//   - scripts/updates/staging/setup-backup-throttle.sh at install time, so
//     re-runs of the installer after a UI change re-materialize the drop-in
//
// Exits 0 on success, non-zero with a message on stderr otherwise.

$SETTINGS = '/docker/whp/settings.json';
$DROPIN_DIR = '/etc/systemd/system/whp-backup.slice.d';
$DROPIN_PATH = $DROPIN_DIR . '/local.conf';
$BACKUP_DIR = '/docker/backups';

function fail($msg) {
    fwrite(STDERR, "apply-backup-throttle: $msg\n");
    exit(1);
}

// Run a command via proc_open (argv form, no shell). Returns exit code;
// stdout/stderr are surfaced via $out / $err. Used instead of shell_exec so
// we don't pull a shell into a security-sensitive helper.
function run(array $argv, &$out = null, &$err = null) {
    $descriptors = [
        0 => ['pipe', 'r'],
        1 => ['pipe', 'w'],
        2 => ['pipe', 'w'],
    ];
    $proc = proc_open($argv, $descriptors, $pipes);
    if (!is_resource($proc)) {
        return -1;
    }
    fclose($pipes[0]);
    $out = stream_get_contents($pipes[1]); fclose($pipes[1]);
    $err = stream_get_contents($pipes[2]); fclose($pipes[2]);
    return proc_close($proc);
}

// Resolve the block device that backs /docker/backups, walking partition ->
// parent disk so io.max applies to the whole device. Reads /proc/mounts and
// /sys/class/block directly; no shell out, no jq/lsblk dependency.
function resolveBlockDevice($path) {
    // Find the longest mountpoint prefix matching $path.
    $mounts = @file('/proc/mounts', FILE_IGNORE_NEW_LINES) ?: [];
    $best = '';
    $bestLen = -1;
    foreach ($mounts as $line) {
        $parts = preg_split('/\s+/', $line);
        if (count($parts) < 2) {
            continue;
        }
        [$dev, $mnt] = [$parts[0], $parts[1]];
        $mntLen = strlen($mnt);
        // Match $mnt as a directory boundary, not a substring (so /docker
        // doesn't match /docker-data when both exist).
        $boundary = ($mnt === '/' || $path === $mnt || strpos($path, rtrim($mnt, '/') . '/') === 0);
        if ($boundary && $mntLen > $bestLen) {
            $best = $dev;
            $bestLen = $mntLen;
        }
    }
    if ($best === '') {
        return null;
    }

    // Walk partition (sda1) -> parent disk (sda) via /sys/class/block.
    $name = basename($best);
    $sys = "/sys/class/block/$name";
    if (is_link($sys) || is_dir($sys)) {
        $real = realpath($sys);
        // /sys/devices/.../block/<disk>/<partition>  ->  parent is <disk>.
        $parent = basename(dirname($real));
        $parentPath = "/dev/$parent";
        if ($parent !== '' && $parent !== 'block' && file_exists($parentPath)) {
            return $parentPath;
        }
    }
    return file_exists($best) ? $best : null;
}

// 1. Load settings.json. Missing file or missing backup_throttle key = a
//    clean "remove the drop-in" signal so the main unit's defaults apply.
$settings = [];
if (file_exists($SETTINGS)) {
    $raw = @file_get_contents($SETTINGS);
    $settings = $raw === false ? [] : (json_decode($raw, true) ?: []);
}
$throttle = $settings['backup_throttle'] ?? null;

if (!is_array($throttle)) {
    if (file_exists($DROPIN_PATH)) {
        @unlink($DROPIN_PATH);
        $rc = run(['/usr/bin/systemctl', 'daemon-reload']);
        exit($rc === 0 ? 0 : 1);
    }
    echo "no backup_throttle in settings.json; nothing to apply\n";
    exit(0);
}

// 2. Validate. Each cap must be a positive integer.
$readMb  = isset($throttle['read_bw_mb'])  ? (int)$throttle['read_bw_mb']  : 0;
$writeMb = isset($throttle['write_bw_mb']) ? (int)$throttle['write_bw_mb'] : 0;
$readIo  = isset($throttle['read_iops'])   ? (int)$throttle['read_iops']   : 0;
$writeIo = isset($throttle['write_iops'])  ? (int)$throttle['write_iops']  : 0;

foreach (['read_bw_mb' => $readMb, 'write_bw_mb' => $writeMb,
          'read_iops' => $readIo, 'write_iops' => $writeIo] as $name => $val) {
    if ($val <= 0) {
        fail("settings.backup_throttle.$name must be a positive integer (got: " . var_export($val, true) . ")");
    }
}

// 3. Resolve the backing block device.
if (!is_dir($BACKUP_DIR)) {
    @mkdir($BACKUP_DIR, 0755, true);
}
$blkDev = resolveBlockDevice($BACKUP_DIR);
if ($blkDev === null) {
    fail("could not resolve block device for $BACKUP_DIR");
}

// 4. Render and atomically install the drop-in. The [Slice] section's IO*Max
//    keys each carry the device path they apply to, so the same device path
//    appears on each line.
$content = "# Generated by apply-backup-throttle.php from /docker/whp/settings.json\n"
    . "# Edit via the WHP UI (Server Settings -> Services -> Backup I/O Throttle).\n"
    . "# Manual edits will be overwritten the next time settings are saved.\n"
    . "[Slice]\n"
    . "IOAccounting=yes\n"
    . "IOReadBandwidthMax=$blkDev {$readMb}M\n"
    . "IOWriteBandwidthMax=$blkDev {$writeMb}M\n"
    . "IOReadIOPSMax=$blkDev $readIo\n"
    . "IOWriteIOPSMax=$blkDev $writeIo\n";

if (!is_dir($DROPIN_DIR) && !@mkdir($DROPIN_DIR, 0755, true) && !is_dir($DROPIN_DIR)) {
    fail("could not create $DROPIN_DIR");
}

$tmp = $DROPIN_PATH . '.tmp.' . getmypid();
if (file_put_contents($tmp, $content) === false) {
    fail("could not write $tmp");
}
if (!@rename($tmp, $DROPIN_PATH)) {
    @unlink($tmp);
    fail("could not move $tmp -> $DROPIN_PATH");
}
@chmod($DROPIN_PATH, 0644);

// 5. Reload systemd. Future transient scopes pick up the drop-in; any scope
//    already running keeps its original cgroup attributes (finishes under
//    the looser caps, next firing gets the new ones).
$rc = run(['/usr/bin/systemctl', 'daemon-reload'], $out, $err);
if ($rc !== 0) {
    fail("systemctl daemon-reload failed (rc=$rc): " . trim((string)$err));
}

echo "applied backup_throttle to $DROPIN_PATH on $blkDev: "
    . "read={$readMb}M write={$writeMb}M rIOPS=$readIo wIOPS=$writeIo\n";
exit(0);
