<?php
/**
 * nest_incremental.php — pull NeST releases since $since via the OCDS `/api/releases` endpoint,
 * upserting each into `crm_nest_tenders`. Cursor-paginated.
 *
 * Usage:
 *   php app/cli/nest_incremental.php
 *   php app/cli/nest_incremental.php --since=2025-07-01T00:00:00
 *   php app/cli/nest_incremental.php --since=... --max-pages=200 --dry-run
 *
 * No args → since = latest release_date in DB (or 2025-06-01 if empty).
 * Exit code: 0 on success, 1 on unrecoverable error.
 */
if (PHP_SAPI !== 'cli') { fwrite(STDERR, "CLI only.\n"); exit(2); }
define('IS_CLI', true);
define('DS', DIRECTORY_SEPARATOR);
define('ROOT', dirname(__DIR__, 2));

spl_autoload_register(function ($cls) {
    foreach (['app/models','app/controllers','core'] as $d) {
        $p = ROOT . '/' . $d . '/' . $cls . '.php';
        if (file_exists($p)) { require $p; return; }
    }
});
require ROOT . '/config/config.php';

$opts = getopt('', ['since::','max-pages::','dry-run::','starting-cursor::']);
$dryRun    = isset($opts['dry-run']);
$maxPages  = isset($opts['max-pages']) ? max(1, (int)$opts['max-pages']) : 40;
$cursor    = isset($opts['starting-cursor']) ? (int)$opts['starting-cursor'] : 0;

$nt = new NestTenders();
$since = $opts['since'] ?? null;
if (!$since) {
    $last = $nt->lastReleaseDate();
    $since = $last ? date('Y-m-d\TH:i:s', strtotime($last)) : '2025-06-01T00:00:00';
}

$logId = $nt->logStart('incremental', $since, null);
echo "[" . date('Y-m-d H:i:s') . "] nest_incremental · since=$since · max-pages=$maxPages · dry-run=" . ($dryRun ? 'yes' : 'no') . " · log_id=$logId" . PHP_EOL;

$base = 'https://nest.go.tz/gateway/nest-data-portal-api/api/releases';
$fetched = 0; $inserted = 0; $updated = 0; $skipped = 0; $errors = 0; $skippedUrls = [];
$pagesDone = 0; $lastCursor = null;
$maxRetries = 4;

while ($pagesDone < $maxPages) {
    $url = $base . '?since=' . urlencode($since) . '&cursor=' . urlencode((string)$cursor);
    $retry = 0; $body = null;

    while ($retry <= $maxRetries) {
        $ch = curl_init($url);
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_TIMEOUT        => 180,
            CURLOPT_CONNECTTIMEOUT => 30,
            CURLOPT_USERAGENT      => 'rsflow.dspera/nest-sync',
            CURLOPT_HTTPHEADER     => ['Accept: application/json'],
            CURLOPT_SSL_VERIFYPEER => false, // WAMP dev boxes rarely have a CA bundle configured
            CURLOPT_SSL_VERIFYHOST => 0,
        ]);
        $body = curl_exec($ch);
        $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $err  = curl_error($ch);
        curl_close($ch);

        if ($body !== false && $code >= 200 && $code < 300) break;

        $wait = [5, 15, 45, 120][$retry] ?? 120;
        $wait += random_int(-1, 1); // small jitter
        echo "  ! HTTP $code · retry=$retry · sleeping {$wait}s · " . substr((string)$err, 0, 80) . PHP_EOL;
        $errors++;
        sleep($wait);
        $retry++;
    }
    if ($body === false || $body === null) {
        $skippedUrls[] = $url;
        echo "  × skip page (cursor=$cursor) after $retry retries" . PHP_EOL;
        $cursor++; $pagesDone++;
        continue;
    }

    $decoded = json_decode($body, true);
    if (!is_array($decoded) || !isset($decoded['releases'])) {
        echo "  × unexpected payload at cursor=$cursor · body head: " . substr($body, 0, 120) . PHP_EOL;
        $skippedUrls[] = $url; $errors++; $cursor++; $pagesDone++;
        continue;
    }

    $releases = $decoded['releases'];
    echo "  page cursor=$cursor · " . count($releases) . " release(s)" . PHP_EOL;

    foreach ($releases as $rel) {
        $fetched++;
        if ($dryRun) { $skipped++; continue; }
        try {
            [$outcome, $ocid] = $nt->upsertFromRelease($rel);
            if ($outcome === 'inserted')      $inserted++;
            elseif ($outcome === 'updated')   $updated++;
            else                              $skipped++;
        } catch (Throwable $e) {
            $errors++;
            echo "    err upsert: " . $e->getMessage() . PHP_EOL;
        }
    }

    // Follow cursor via links.next if present, else advance +1 as fallback
    $next = $decoded['links']['next'] ?? null;
    if (is_string($next) && preg_match('/[?&]cursor=(\d+)/', $next, $m)) {
        $cursor = (int)$m[1];
    } else {
        // No more pages
        $lastCursor = $cursor;
        echo "  · no next link · done" . PHP_EOL;
        break;
    }
    $lastCursor = $cursor;
    $pagesDone++;
}

$nt->logFinish($logId, [
    'fetched' => $fetched, 'inserted' => $inserted, 'updated' => $updated,
    'skipped' => $skipped, 'errors' => $errors,
    'cursor_end' => (string)$lastCursor, 'page_end' => $pagesDone,
], $errors > 0 && $inserted + $updated === 0 ? 'failed' : 'done',
    "pages=$pagesDone · since=$since", $skippedUrls);

echo "[" . date('Y-m-d H:i:s') . "] done · fetched=$fetched · inserted=$inserted · updated=$updated · skipped=$skipped · errors=$errors · pages=$pagesDone" . PHP_EOL;
exit($errors > 0 && ($inserted + $updated) === 0 ? 1 : 0);
