<?php
// ═══════════════════════════════════════════════════════════════════════════════════════════════
// PURPOSE
//   Container update worker. Pulls one image, decides whether it actually changed, and
//   recreates the container if it did — reporting progress through a job file the docker tab
//   polls.
//
// OPERATIONAL MODEL
//   Not an HTTP endpoint. This runs as a detached CLI process, spawned by docker_action.php,
//   because a pull can take minutes and no web request should be held open for it. It lives
//   under api/ because it is part of that endpoint's implementation, not because it is
//   reachable over HTTP — and it refuses to run if it ever is.
//
//   Progress is a file, not a return value. The parent request returns immediately with a job
//   id; this process writes the current state to that file as it goes, and the page polls it.
//   The file is the only channel between the two.
//
//   Called as: php docker_pull_worker.php <name> <jobFile> <oldId> <image> <rebuild>
//
// DESIGN PRINCIPLES
//   Compares image ids, not pull output.
//       `docker pull` reports success whether or not anything changed. The image id before
//       and after is the only reliable signal, and it is what decides whether the container
//       is disturbed at all — an up-to-date container is never restarted.
//
//   Prefers Unraid's own rebuild path.
//       When a rebuild helper is supplied and executable it is used, because recreating a
//       container correctly means reapplying its full template — ports, mounts, variables.
//       The stop/start fallback exists for the case where that helper is unavailable, and is
//       explicitly the lesser option: it picks up a new image only if the container was
//       already configured to be recreated on start.
//
//   Every exit writes a terminal state.
//       Both outcomes end with a job-file write, so the poller always converges. A worker that
//       died without writing would leave the page spinning indefinitely.
//
// OPERATIONAL SAFEGUARDS
//   Refuses to run under a web server.
//       PHP_SAPI is checked first and a non-CLI invocation is answered with a 404 and no
//       output. Without that, requesting this file over HTTP would evaluate it with no $argv
//       at all — and it is a script whose entire job is to stop and restart containers.
//
//   Required arguments are checked before anything runs.
//       Missing name, job file, or image exits non-zero before the first docker call, so a
//       malformed spawn cannot pull or restart anything.
//
//   Every value interpolated into a shell command is escaped.
//       Image, container name and the rebuild helper path all go through escapeshellarg(),
//       even though they originate from docker_action.php rather than from a request. The
//       escaping is what stays correct if that caller ever changes.
//
//   The rebuild helper is confirmed executable before it is invoked.
//       is_executable() gates it, so a missing or non-executable helper falls back to
//       stop/start rather than failing the update with a shell error.
//
//   Failure is reported as failure.
//       A non-zero rebuild status writes ok:false with an explicit message. The image has
//       already been pulled at that point, so silently reporting success would leave a
//       container running an old image that the page claims was updated.
//
//   Docker output is captured, never echoed.
//       Every call redirects stderr and the output is discarded or kept locally. This process
//       has no stdout consumer; writing to it would only risk corrupting the job file if the
//       two were ever pointed at the same place.
//
// ARGUMENTS
//   1 name      container name
//   2 jobFile   path the progress JSON is written to
//   3 oldId     image id before the pull, for the changed/unchanged comparison
//   4 image     image reference to pull
//   5 rebuild   optional path to Unraid's container rebuild helper
//
// JOB FILE STATES
//   {"ok":true,"status":"done","updated":false,"message":"Already up to date"}
//   {"ok":true,"status":"rebuilding"}
//   {"ok":true,"status":"done","updated":true,"message":"Updated and rebuilt"}
//   {"ok":false,"status":"done","error":"Rebuild failed after pull"}
//
// DEPENDS ON
//   api/docker_action.php   spawns this worker and creates the job file path
//   docker CLI              pull, image inspect, stop, start
// ═══════════════════════════════════════════════════════════════════════════════════════════════

// This file stops and starts containers. It is a CLI worker and must never be reachable as a
// web request — over HTTP there is no $argv, and every argument below would be undefined.
if (PHP_SAPI !== 'cli') {
    http_response_code(404);
    exit(1);
}

[$name, $jobFile, $oldId, $image, $rebuild] = array_slice($argv, 1, 5) + array_fill(0, 5, '');

if (!$name || !$jobFile || !$image) exit(1);

function jw(string $f, array $d): void { file_put_contents($f, json_encode($d)); }

shell_exec('docker pull ' . escapeshellarg($image) . ' 2>&1');

$rawInfo = shell_exec('docker image inspect ' . escapeshellarg($image) . ' 2>/dev/null') ?: '[]';
$info    = json_decode($rawInfo, true) ?: [];
$newId   = $info[0]['Id'] ?? '';

if ($oldId && $newId && $oldId === $newId) {
    jw($jobFile, ['ok' => true, 'status' => 'done', 'updated' => false, 'message' => 'Already up to date']);
    exit;
}

jw($jobFile, ['ok' => true, 'status' => 'rebuilding']);

if ($rebuild && is_executable($rebuild)) {
    exec(escapeshellarg($rebuild) . ' ' . escapeshellarg($name) . ' 2>&1', $out, $rc);
} else {
    exec('docker stop '  . escapeshellarg($name) . ' 2>&1', $o1, $rc1);
    exec('docker start ' . escapeshellarg($name) . ' 2>&1', $o2, $rc2);
    $rc = ($rc1 === 0 && $rc2 === 0) ? 0 : 1;
}

jw($jobFile, $rc === 0
    ? ['ok' => true,  'status' => 'done', 'updated' => true,  'message' => 'Updated and rebuilt']
    : ['ok' => false, 'status' => 'done', 'error'   => 'Rebuild failed after pull']
);
