<?php
/*
 * test_build_skewt.php
 * Version: 1.01
 *
 * Quick-and-dirty test harness for build_skewt.py. NOT a production wrapper:
 * it does no parameter validation, no whitelisting, and accepts nothing from
 * the query string. Everything it passes to the Python script is hardwired in
 * the CONFIGURATION block below. Edit, reload, look at the output.
 *
 * It runs the script with proc_open so that stdout and stderr stay separate,
 * which is the whole point of the exercise: build_skewt.py puts the finished
 * file path on stdout and everything else -- progress, warnings, ERROR lines --
 * on stderr. Exit codes are 0 success, 2 bad parameter, 3 model not
 * implemented, 4 upstream (NOMADS / GRIB), 5 plotting.
 *
 * Changes from 1.00:
 * - The child process now gets an explicit working directory and an explicit
 *   environment, matching what plot_skewt.php does for make_skewt.py. Without
 *   this the child inherits Apache's environment, where HOME is /var/www --
 *   which www-data cannot write -- so matplotlib rebuilds its font cache in
 *   /tmp on every single run and complains about it on stderr. HOME,
 *   MPLCONFIGDIR and the XDG_* variables are pointed at a scratch directory
 *   instead, so the cache persists between runs and stderr stays clean enough
 *   that real warnings are visible.
 * - The displayed image location is now taken from the path build_skewt.py
 *   prints on stdout rather than being hardwired here, so the Python script's
 *   OUTPUT_DIR stays the single source of truth. Only the web path prefix is
 *   still configured below, since a filesystem path cannot be turned into a
 *   URL without knowing the document root mapping.
 */

// ============================================================================
// CONFIGURATION
// ============================================================================

// Interpreter and script. This is the same virtualenv plot_skewt.php uses for
// make_skewt.py; keep the two in step so the tools cannot drift onto different
// MetPy versions. Inside a venv, 'python' and 'python3' are the same binary.
$PYTHON      = '/home/ubuntu/test/weather/venv/bin/python';
$SCRIPT      = '/home/ubuntu/weather/products/graphics/soundings/build_skewt.py';

// Working directory for the child process.
$PYTHON_CWD  = '/home/ubuntu/weather/products/graphics/soundings';

// Scratch directory used as HOME / MPLCONFIGDIR / XDG_CACHE_HOME for the child.
// Shared with plot_skewt.php on purpose: one warm matplotlib font cache.
$SCRATCH_HOME = '/tmp/skewt_cache';

// Filesystem directory build_skewt.py writes to (its OUTPUT_DIR), and the
// matching web path. Only used to turn the path the script reports into a URL
// for display below.
$OUTPUT_DIR  = '/var/www/html/weather/analysis_graphics';
$WEB_PATH    = '/weather/analysis_graphics';

// Seconds to wait before giving up. NOMADS can be slow; build_skewt.py's own
// download timeout is 90 s per cycle attempt and it may try up to 4 cycles.
$TIMEOUT     = 420;

// --- Parameters passed to build_skewt.py ------------------------------------
// Set a value to null to omit that switch entirely (useful for testing the
// "required parameter missing" error paths, and for the optional loc/trs).
$PARAMS = [
    'model'       => 'nam',
    'lat'         => '44.0581',
    'lon'         => '-121.3153',
    'loc'         => 'Bend, Oregon',
    'trs'         => 'T17S R12E Section 16',
    'fhr'         => '21',
    'output_file' => 'test_skewt_bend_f21',
];

// Alternatively, drive the script from a parameter file. If this is non-null,
// it is passed as --file and supersedes everything in $PARAMS above.
$PARAM_FILE = null;   // e.g. '/home/ubuntu/weather/products/graphics/soundings/test_params.csv'

// ============================================================================
// HELPERS
// ============================================================================

function ensure_dir($path) {
    if (!is_dir($path) && !@mkdir($path, 0775, true)) {
        return false;
    }
    return is_writable($path);
}

// ============================================================================
// BUILD THE COMMAND
// ============================================================================

$argv = [escapeshellarg($PYTHON), escapeshellarg($SCRIPT)];

if ($PARAM_FILE !== null) {
    $argv[] = '--file=' . escapeshellarg($PARAM_FILE);
} else {
    foreach ($PARAMS as $key => $value) {
        if ($value === null) { continue; }
        $argv[] = '--' . $key . '=' . escapeshellarg($value);
    }
}

$cmd = implode(' ', $argv);

// ============================================================================
// BUILD THE CHILD ENVIRONMENT
// ============================================================================
//
// proc_open with a null env hands the child a copy of Apache's environment,
// where HOME is /var/www and unwritable by www-data. Everything that wants a
// dot-directory then falls over -- matplotlib most loudly. Build the
// environment explicitly instead.

$env = [];
foreach ($_SERVER as $k => $v) {
    if (is_string($v) && preg_match('/^[A-Z_][A-Z0-9_]*$/', $k)) {
        $env[$k] = $v;
    }
}
$env['PATH']            = $env['PATH'] ?? '/usr/local/bin:/usr/bin:/bin';
$env['LANG']            = $env['LANG'] ?? 'C.UTF-8';
$env['LC_ALL']          = $env['LC_ALL'] ?? 'C.UTF-8';
$env['HOME']            = $SCRATCH_HOME;
$env['MPLCONFIGDIR']    = $SCRATCH_HOME . '/matplotlib';
$env['XDG_CACHE_HOME']  = $SCRATCH_HOME . '/cache';
$env['XDG_CONFIG_HOME'] = $SCRATCH_HOME . '/config';
// Running <venv>/bin/python directly is enough to select the venv, but setting
// VIRTUAL_ENV keeps anything that inspects it from getting confused.
$env['VIRTUAL_ENV']     = dirname(dirname($PYTHON));

$envProblems = [];
foreach ([$SCRATCH_HOME, $env['MPLCONFIGDIR'],
          $env['XDG_CACHE_HOME'], $env['XDG_CONFIG_HOME']] as $d) {
    if (!ensure_dir($d)) {
        $envProblems[] = "not writable: {$d}";
    }
}

// ============================================================================
// RUN IT
// ============================================================================

$descriptors = [
    0 => ['pipe', 'r'],   // stdin  (unused)
    1 => ['pipe', 'w'],   // stdout -- the output path
    2 => ['pipe', 'w'],   // stderr -- progress, warnings, ERROR lines
];

$started  = microtime(true);
$stdout   = '';
$stderr   = '';
$exitCode = null;
$timedOut = false;

$proc = proc_open($cmd, $descriptors, $pipes, $PYTHON_CWD, $env);

if (!is_resource($proc)) {
    $stderr   = "Could not start process.\n";
    $exitCode = -1;
} else {
    fclose($pipes[0]);
    stream_set_blocking($pipes[1], false);
    stream_set_blocking($pipes[2], false);

    // Read both pipes until they close, so a chatty stderr cannot fill its
    // buffer and deadlock the child while we sit waiting on stdout.
    while (true) {
        $read = [];
        if (!feof($pipes[1])) { $read[] = $pipes[1]; }
        if (!feof($pipes[2])) { $read[] = $pipes[2]; }
        if (empty($read)) { break; }

        if (microtime(true) - $started > $TIMEOUT) {
            $timedOut = true;
            proc_terminate($proc, 9);
            break;
        }

        $write = $except = null;
        if (stream_select($read, $write, $except, 1) === false) { break; }

        foreach ($read as $pipe) {
            $chunk = fread($pipe, 8192);
            if ($chunk === false || $chunk === '') { continue; }
            if ($pipe === $pipes[1]) { $stdout .= $chunk; }
            else                     { $stderr .= $chunk; }
        }
    }

    fclose($pipes[1]);
    fclose($pipes[2]);
    $exitCode = proc_close($proc);
}

$elapsed = microtime(true) - $started;

// ============================================================================
// INTERPRET THE RESULT
// ============================================================================
//
// build_skewt.py writes the finished path to stdout and nothing else, so
// success needs no mtime comparison and no hardwired output filename here.
// The script also installs its PNG with os.replace(), which only needs write
// permission on the DIRECTORY -- so unlike make_skewt.py there is no stale
// root-owned output file to unlink first.

$outputPath = trim($stdout);
$ok         = ($exitCode === 0 && $outputPath !== '');

$imgUrl  = '';
$imgNote = '';
if ($ok) {
    clearstatcache();
    if (!is_file($outputPath)) {
        $imgNote = "Script reported success but {$outputPath} does not exist.";
    } else {
        $realOut = realpath($outputPath);
        $realDir = realpath($OUTPUT_DIR);
        $prefix  = rtrim((string)$realDir, '/') . '/';
        if ($realDir !== false
                && strncmp($realOut, $prefix, strlen($prefix)) === 0) {
            $imgUrl = rtrim($WEB_PATH, '/') . '/'
                    . rawurlencode(substr($realOut, strlen($prefix)))
                    . '?t=' . filemtime($realOut);
        } else {
            $imgNote = "Output landed at {$realOut}, which is not under "
                     . "\$OUTPUT_DIR ({$OUTPUT_DIR}); cannot build a URL. "
                     . "Check that OUTPUT_DIR in build_skewt.py matches.";
        }
    }
}

// ============================================================================
// SHOW THE RESULTS
// ============================================================================
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>build_skewt.py test harness</title>
<style>
  body   { font-family: system-ui, sans-serif; margin: 1.5rem; max-width: 1250px; }
  h1     { font-size: 1.2rem; }
  h2     { font-size: 0.95rem; margin: 1.2rem 0 0.3rem; text-transform: uppercase;
           letter-spacing: 0.05em; color: #555; }
  pre    { background: #f4f4f4; border: 1px solid #ddd; padding: 0.6rem;
           white-space: pre-wrap; word-break: break-all; font-size: 0.8rem; }
  .ok    { color: #060; font-weight: bold; }
  .bad   { color: #a00; font-weight: bold; }
  img    { max-width: 100%; border: 1px solid #ccc; margin-top: 0.4rem; }
</style>
</head>
<body>

<h1>build_skewt.py test harness</h1>

<p>
  Exit code:
  <span class="<?= $ok ? 'ok' : 'bad' ?>"><?= htmlspecialchars((string)$exitCode) ?></span>
  <?php
    $meanings = [
        0 => 'success', 2 => 'bad parameter', 3 => 'model not implemented',
        4 => 'upstream (NOMADS / GRIB)', 5 => 'plotting failure',
    ];
    if (isset($meanings[$exitCode])) {
        echo ' &mdash; ' . htmlspecialchars($meanings[$exitCode]);
    }
    if ($timedOut) { echo ' <span class="bad">(TIMED OUT)</span>'; }
  ?>
  &nbsp;|&nbsp; elapsed <?= number_format($elapsed, 1) ?> s
</p>

<?php if (!empty($envProblems)): ?>
<p class="bad">Scratch directory problems:
   <?= htmlspecialchars(implode('; ', $envProblems)) ?></p>
<?php endif; ?>

<h2>Command</h2>
<pre><?= htmlspecialchars($cmd) ?></pre>

<h2>Child environment</h2>
<pre><?php
  foreach (['HOME','MPLCONFIGDIR','XDG_CACHE_HOME','XDG_CONFIG_HOME',
            'VIRTUAL_ENV','PATH','LANG'] as $k) {
      printf("%-16s %s\n", $k, htmlspecialchars($env[$k] ?? '(unset)'));
  }
  printf("%-16s %s\n", 'cwd', htmlspecialchars($PYTHON_CWD));
  printf("%-16s %s\n", 'PHP user',
      htmlspecialchars(function_exists('posix_getpwuid')
          ? (posix_getpwuid(posix_geteuid())['name'] ?? '?')
          : get_current_user()));
?></pre>

<h2>stdout (output path)</h2>
<pre><?= $stdout === '' ? '(empty)' : htmlspecialchars($stdout) ?></pre>

<h2>stderr (progress / warnings / errors)</h2>
<pre><?= $stderr === '' ? '(empty)' : htmlspecialchars($stderr) ?></pre>

<h2>Image</h2>
<?php if ($imgUrl !== ''): ?>
  <img src="<?= htmlspecialchars($imgUrl) ?>" alt="Skew-T">
<?php elseif ($imgNote !== ''): ?>
  <p class="bad"><?= htmlspecialchars($imgNote) ?></p>
<?php else: ?>
  <p>(no image produced)</p>
<?php endif; ?>

</body>
</html>
<?php
# --- END OF FILE: test_build_skewt.php Version 1.01 ---