<?php
declare(strict_types=1);

// DRAFT ONLY. A reviewer must approve this exact SHA and explicit token before use.
// No invocation in the current evidence round sets this token.
const CDX_READ2_APPROVAL = 'CDX_READ2_HTTP_APPROVED_20261002_V1';
if (!hash_equals(CDX_READ2_APPROVAL, (string)getenv('CDX_READ2_HTTP_APPROVAL'))) {
    echo json_encode(['status' => 'default_deny', 'business_http_sent' => false,
        'database_connected' => false, 'dml_sent' => false],
        JSON_UNESCAPED_SLASHES), PHP_EOL;
    exit(78);
}

$target = (string)getenv('CDX_READ2_TARGET');
if (!in_array($target, ['A', 'B', 'C'], true)) {
    throw new RuntimeException('Explicit target required');
}
putenv('CDX_READ2_SCHEMA_PREFLIGHT=1');
require __DIR__ . '/read2_schema_preflight.php';

// The preflight above establishes the A/B local or C nonproduction two-DB identity,
// expanded transitive table dependencies, zero old marker prefix, and external stubs.
$runtimeRoot = [
    'A' => '/private/tmp/zhct-http-ai-macos.zjaU8h',
    'B' => '/private/tmp/zhct-http-ai-macos.x2CCZX',
    'C' => '/tmp/zhct-ai-http-20260929.3XDj3w',
][$target];
$port = ['A' => 18105, 'B' => 18101, 'C' => 18102][$target];
$cacheConfigFile = $runtimeRoot . '/config/cache.php';
$approvedCacheConfigSha = 'ccf1c56eb4178f3eaad8775e26a27742b2cacf662b46fb058cbb91d23783b8c6';
$rootReal = realpath($runtimeRoot);
$cacheReal = realpath($runtimeRoot . '/runtime/cache');
if (!is_string($rootReal) || !is_string($cacheReal)
    || !hash_equals($approvedCacheConfigSha, (string)hash_file('sha256', $cacheConfigFile))
    || $cacheReal !== $rootReal . '/runtime/cache'
    || (((int)fileperms($rootReal) & 0077) !== 0)
    || is_file($rootReal . '/.env') || is_file($rootReal . '/.env.poc')
    || getenv('CACHE_DRIVER') !== false || getenv('cache.driver') !== false) {
    throw new RuntimeException('Private file cache boundary unproven');
}
// The existing AI POC boundary only reports external_stubs, not redis_isolated.
// We therefore never infer Redis safety from it: the HTTP process must have no
// cache-driver override and both CLI/HTTP must use this root-local File store.
$httpCacheEnvironment = $target === 'C' ? ($raw ?? '') : ($cmdline ?? '');
if (!is_string($httpCacheEnvironment)
    || preg_match('/(?:^|[\s\x00])CACHE_DRIVER=/i', $httpCacheEnvironment) === 1
    || preg_match('/(?:^|[\s\x00])cache\.driver=/i', $httpCacheEnvironment) === 1) {
    throw new RuntimeException('HTTP cache driver override is not isolated');
}
foreach ($config as $key => $value) {
    if (strpos($key, 'POC_') === 0) putenv($key . '=' . $value);
}
define('TP_ENV', 'poc');
define('BUSINESS', 'aizhct');
define('ENTERPRISE_NO', '10001');
require $runtimeRoot . '/vendor/autoload.php';
(new \think\App($runtimeRoot))->setEnvName('poc')->initialize();
$effectiveCacheDriver = \think\facade\Config::get('cache.default');
$effectiveCachePath = \think\facade\Config::get('cache.stores.file.path');
$effectiveCachePrefix = \think\facade\Config::get('cache.stores.file.prefix');
if (strtolower((string)$effectiveCacheDriver) !== 'file'
    || realpath((string)$effectiveCachePath) !== $cacheReal
    || (string)$effectiveCachePrefix !== '') {
    throw new RuntimeException('CLI cache store does not match private HTTP file store');
}

$ai->setAttribute(PDO::ATTR_CASE, PDO::CASE_LOWER);
$store->setAttribute(PDO::ATTR_CASE, PDO::CASE_LOWER);
$aiUser = $dialect === 'dm' ? 'YOSHOP_USER' : 'yoshop_user';
$aiStaff = $dialect === 'dm' ? 'YOSHOP_STAFF' : 'yoshop_staff';
$aiSubsidy = $dialect === 'dm' ? 'YOSHOP_USER_SUBSIDY' : 'yoshop_user_subsidy';
$aiOauth = $dialect === 'dm' ? 'YOSHOP_USER_OAUTH' : 'yoshop_user_oauth';
$aiBalanceLog = $dialect === 'dm' ? 'YOSHOP_USER_BALANCE_LOG' : 'yoshop_user_balance_log';
$storeUser = $dialect === 'dm' ? 'YDY_USER' : 'ydy_user';
$identity = [
    'target' => $target,
    'runtime_root' => realpath($runtimeRoot),
    'http_port' => $port,
    'http_pid' => $target === 'C' ? 53772 : (int)$pids[0],
];
foreach (['POC_AI_HOST', 'POC_AI_PORT', 'POC_AI_SCHEMA', 'POC_AI_USER',
    'POC_HOST', 'POC_PORT', 'POC_SCHEMA', 'POC_USER'] as $key) {
    $identity[$key] = (string)($config[$key] ?? '');
    if ($identity[$key] === '') {
        throw new RuntimeException('Non-secret database identity incomplete');
    }
}
$identityJson = json_encode($identity, JSON_UNESCAPED_SLASHES);
if (!is_string($identityJson) || !is_string($identity['runtime_root'])) {
    throw new RuntimeException('Non-secret database identity unavailable');
}
$identitySha = hash('sha256', $identityJson);
$marker = 'CDXPARREAD2' . strtoupper(bin2hex(random_bytes(5)));
$token = 'CDXPARREAD2T' . strtoupper(bin2hex(random_bytes(16)));
$journalDir = sys_get_temp_dir() . '/cdx-read2-' . strtolower(bin2hex(random_bytes(8)));
if (!mkdir($journalDir, 0700)) {
    throw new RuntimeException('Private cleanup journal unavailable');
}
$journal = $journalDir . '/manifest.json';
$journalState = [
    'target' => $target, 'marker' => $marker, 'phase' => 'before_insert',
    'db_identity_sha256' => $identitySha,
    'ai_user_id' => 0, 'ai_staff_id' => 0,
    'original_user_sha256' => null, 'original_staff_sha256' => null,
    'token_ttl_seconds' => 300, 'token_issued_at' => null,
    'token_expires_at' => null, 'token_set_completed_at' => null,
    'token_value_stored' => false,
    'recovery_rule' => 'wait_token_expiry_plus_margin_then_single_worker_barrier_and_exact_relation_check',
];
$saveJournal = static function (array $changes) use (&$journalState, $journalDir,
    $journal): void {
    $next = array_merge($journalState, $changes);
    $temp = tempnam($journalDir, 'next-');
    if (!is_string($temp) || !chmod($temp, 0600)) {
        throw new RuntimeException('Private journal temp unavailable');
    }
    $json = json_encode($next, JSON_UNESCAPED_SLASHES);
    if (!is_string($json) || file_put_contents($temp, $json, LOCK_EX) !== strlen($json)
        || !rename($temp, $journal)) {
        throw new RuntimeException('Private atomic journal write failed');
    }
    $journalState = $next;
};
$saveJournal([]);
$userId = 0;
$staffId = 0;
$tokenSaved = false;
$requestComplete = true;
$result = ['target' => $target, 'cases' => [], 'cleanup' => 'not_started'];

$readRows = static function (PDO $pdo, string $sql, array $bind = []): array {
    $statement = $pdo->prepare($sql);
    $statement->execute($bind);
    return $statement->fetchAll(PDO::FETCH_ASSOC);
};
$count = static function (PDO $pdo, string $sql, array $bind = [])
    use ($readRows): int {
    $rows = $readRows($pdo, $sql, $bind);
    return count($rows) === 1 ? (int)array_values($rows[0])[0] : -1;
};
$fingerprint = static function (array $row): string {
    ksort($row);
    return hash('sha256', json_encode($row, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
};
$getOwned = static function (string $table, string $idColumn, int $id,
    string $ownerColumn, int $owner) use ($ai, $readRows): array {
    $rows = $readRows($ai, 'SELECT * FROM ' . $table . ' WHERE ' . $idColumn
        . '=? AND ' . $ownerColumn . '=?', [$id, $owner]);
    if (count($rows) !== 1) throw new RuntimeException('Synthetic row ownership mismatch');
    return $rows[0];
};
$request = static function (string $path) use ($port, $token, &$requestComplete): array {
    $allowed = ['/api/User/info', '/api/User/assets'];
    if (!in_array($path, $allowed, true)) {
        throw new RuntimeException('Business route outside read-only whitelist');
    }
    $context = stream_context_create(['http' => [
        'method' => 'GET', 'timeout' => 25, 'ignore_errors' => true,
        'header' => 'Access-Token: ' . $token . "\r\n"
            . "platform: MP-WEIXIN\r\n"
            . "version: 2.0.51\r\n"
            . "env-version: develop\r\n",
    ]]);
    $requestComplete = false;
    $startedAt = microtime(true);
    try {
        $text = file_get_contents('http://127.0.0.1:' . $port . $path
            . '?storeId=10001', false, $context);
    } catch (Throwable $transportError) {
        return ['http' => 0, 'body' => null,
            'elapsed_ms' => (int)round((microtime(true) - $startedAt) * 1000)];
    }
    $elapsedMs = (int)round((microtime(true) - $startedAt) * 1000);
    $headerLine = $http_response_header[0] ?? '';
    $status = preg_match('/HTTP\/\S+\s+(\d+)/', $headerLine, $match)
        ? (int)$match[1] : 0;
    $body = is_string($text) ? json_decode($text, true) : null;
    $requestComplete = $status === 200 && is_array($body);
    return ['http' => $status, 'body' => $body, 'elapsed_ms' => $elapsedMs];
};
$typed = static function ($value): string {
    if (is_string($value) && preg_match('/^-?\d+\.\d+$/', $value)) {
        return 'decimal_string_' . strlen(substr(strrchr($value, '.'), 1)) . 'dp';
    }
    return gettype($value);
};
$keyDigest = static function (array $row): string {
    $keys = array_map('strval', array_keys($row));
    sort($keys, SORT_STRING);
    return hash('sha256', implode("\0", $keys));
};
$hasForbiddenKey = null;
$hasForbiddenKey = static function ($value) use (&$hasForbiddenKey): bool {
    if (!is_array($value)) return false;
    $forbidden = ['password', 'pwd', 'salt', 'api_key', 'secret',
        'access_token', 'refresh_token', 'authorization'];
    foreach ($value as $key => $item) {
        if (is_string($key) && in_array(strtolower($key), $forbidden, true)) return true;
        if ($hasForbiddenKey($item)) return true;
    }
    return false;
};
$rowHashes = [];
$failed = null;
try {
    if ($count($ai, 'SELECT COUNT(*) FROM ' . $aiUser
        . ' WHERE NICK_NAME=? AND STORE_ID=?', [$marker, 10001]) !== 0) {
        throw new RuntimeException('Synthetic marker collision');
    }
    $now = time();
    $ai->prepare('INSERT INTO ' . $aiUser
        . ' (STORE_ID,MOBILE,NICK_NAME,BALANCE,GIFT_BALANCE,IS_DELETE,CREATE_TIME)'
        . ' VALUES (10001,?,?,?, ?,0,?)')
        ->execute(['', $marker, '0.00', '0.00', $now]);
    $users = $readRows($ai, 'SELECT USER_ID FROM ' . $aiUser
        . ' WHERE NICK_NAME=? AND STORE_ID=10001', [$marker]);
    if (count($users) !== 1) throw new RuntimeException('Synthetic AI user missing');
    $userId = (int)$users[0]['user_id'];
    if ($userId <= 0) throw new RuntimeException('Invalid synthetic AI user id');
    $rowHashes['user'] = $fingerprint($getOwned($aiUser, 'USER_ID', $userId,
        'STORE_ID', 10001));
    $saveJournal(['phase' => 'user_inserted', 'ai_user_id' => $userId,
        'original_user_sha256' => $rowHashes['user']]);

    $ai->prepare('INSERT INTO ' . $aiStaff
        . ' (NAME,USER_ID,STORE_ID,HEIGHT,WEIGHT,GENDER,BIRTHDAY,PHONE,IS_DELETE)'
        . " VALUES (?,?,10001,170,60,1,'1990-01-01','',0)")
        ->execute([$marker, $userId]);
    $staffRows = $readRows($ai, 'SELECT ID FROM ' . $aiStaff
        . ' WHERE USER_ID=? AND STORE_ID=10001 AND NAME=?', [$userId, $marker]);
    if (count($staffRows) !== 1) throw new RuntimeException('Synthetic staff missing');
    $staffId = (int)$staffRows[0]['id'];
    $rowHashes['staff'] = $fingerprint($getOwned($aiStaff, 'ID', $staffId,
        'USER_ID', $userId));
    $saveJournal(['phase' => 'staff_inserted', 'ai_staff_id' => $staffId,
        'original_staff_sha256' => $rowHashes['staff']]);
    // Diagnostic of the exact read used by User/info; never expose row values.
    $staffOrm = \app\api\model\staff\Staff::getOne([
        'user_id' => $userId, 'is_delete' => 0,
    ]);
    $result['staff_orm_relation_found_in_cli'] = !empty($staffOrm);
    if (empty($staffOrm)) {
        try {
            $direct = (new \app\api\model\staff\Staff)
                ->where(['user_id' => $userId, 'is_delete' => 0])
                ->field('id,weight')->find();
            $result['staff_orm_direct_query_found_in_cli'] = !empty($direct);
        } catch (Throwable $probeError) {
            $result['staff_orm_direct_query_error_class'] = get_class($probeError);
            $result['staff_orm_direct_query_error_code'] = (string)$probeError->getCode();
        }
    }

    // Same cache envelope as Login::setSession, but a short-lived unique
    // synthetic token, never a real user's token or login endpoint.
    $storeVersionRows = $readRows($store, 'SELECT PASSWORD_VERSION FROM '
        . $storeUser . ' WHERE AI_USER_ID=? AND IS_DELETE=0', [$userId]);
    if ($storeVersionRows !== []) throw new RuntimeException('Unexpected Store user mapping');
    $syntheticUser = \app\api\model\User::detail($userId);
    if (empty($syntheticUser)) throw new RuntimeException('Synthetic login model unavailable');
    $tokenIssuedAt = time();
    $saveJournal(['phase' => 'before_token_set',
        'token_issued_at' => $tokenIssuedAt,
        'token_expires_at' => $tokenIssuedAt + 300]);
    $tokenSaved = \think\facade\Cache::set($token, [
        'user' => $syntheticUser,
        'store_id' => 10001,
        'is_login' => true,
        'password_version' => 0,
        'login_type' => 'sms',
    ], 300);
    if (!$tokenSaved) throw new RuntimeException('Synthetic token cache unavailable');
    $saveJournal(['phase' => 'token_saved', 'token_set_completed_at' => time()]);

    $cases = [
        'info' => '/api/User/info',
        'assets' => '/api/User/assets',
    ];
    foreach ($cases as $label => $path) {
        $saveJournal(['phase' => 'before_' . $label . '_request']);
        $response = $request($path);
        $saveJournal(['phase' => $label . '_response_complete']);
        if (($response['elapsed_ms'] ?? PHP_INT_MAX) >= 15000) {
            $result['cases'][$label] = [
                'http' => $response['http'],
                'elapsed_ms' => $response['elapsed_ms'],
                'within_app_15s_budget' => false,
            ];
            throw new RuntimeException('App request budget exceeded');
        }
        $body = $response['body'];
        $data = is_array($body) ? ($body['data'] ?? null) : null;
        if ($response['http'] !== 200 || !is_array($body)
            || (int)($body['status'] ?? -1) !== 200
            || !is_array($data) || $hasForbiddenKey($data)) {
            $result['cases'][$label] = [
                'http' => $response['http'],
                'elapsed_ms' => $response['elapsed_ms'],
                'within_app_15s_budget' => $response['elapsed_ms'] < 15000,
            ];
            throw new RuntimeException('Synthetic read response contract failed');
        }
        if ($label === 'info') {
            $item = $data['userInfo'] ?? null;
            if (!is_array($item) || (int)($item['user_id'] ?? 0) !== $userId) {
                throw new RuntimeException('User info synthetic identity mismatch');
            }
            $result['cases'][$label] = [
                'http' => 200, 'status' => 200,
                'elapsed_ms' => $response['elapsed_ms'],
                'within_app_15s_budget' => true,
                'user_id_matches_original' => true,
                'weight_matches_synthetic_staff_60' => isset($item['weight'])
                    && is_numeric($item['weight'])
                    && abs((float)$item['weight'] - 60.0) < 0.001,
                'weight_is_fallback_zero' => ($item['weight'] ?? null) === 0,
                'field_types' => [
                    'user_id' => $typed($item['user_id']),
                    'store_id' => $typed($item['store_id'] ?? null),
                    'weight' => $typed($item['weight'] ?? null),
                    'sportData.step' => $typed($item['sportData']['step'] ?? null),
                    'sportData.energy' => $typed($item['sportData']['energy'] ?? null),
                    'oneCardEnabled' => $typed($item['oneCardEnabled'] ?? null),
                    'accountType' => $typed($item['accountType'] ?? null),
                    'department_name' => $typed($item['department_name'] ?? null),
                ],
                'top_level_key_count' => count($item),
                'top_level_key_sha256' => $keyDigest($item),
            ];
        } else {
            $asset = $data['assets'] ?? null;
            if (!is_array($asset)) throw new RuntimeException('Assets object missing');
            foreach (['balance', 'subsidy', 'gift_balance', 'total_balance'] as $money) {
                if (!isset($asset[$money]) || !is_numeric($asset[$money])
                    || (float)$asset[$money] !== 0.0) {
                    throw new RuntimeException('Synthetic account not zero');
                }
            }
            $types = [];
            foreach (['balance', 'points', 'coupon', 'subsidy', 'gift_balance',
                'total_balance'] as $field) {
                $types[$field] = $typed($asset[$field] ?? null);
            }
            $result['cases'][$label] = [
                'http' => 200, 'status' => 200,
                'elapsed_ms' => $response['elapsed_ms'],
                'within_app_15s_budget' => true,
                'zero_funds_match_original' => true,
                'field_types' => $types,
                'top_level_key_count' => count($asset),
                'top_level_key_sha256' => $keyDigest($asset),
                'origin_department_keys_present' => is_array($asset['origin_department'] ?? null),
            ];
        }
    }

    if ($count($ai, 'SELECT COUNT(*) FROM ' . $aiSubsidy
        . ' WHERE USER_ID=? AND STORE_ID=10001', [$userId]) !== 0
        || $count($ai, 'SELECT COUNT(*) FROM ' . $aiOauth
            . ' WHERE USER_ID=?', [$userId]) !== 0
        || $count($ai, 'SELECT COUNT(*) FROM ' . $aiBalanceLog
            . ' WHERE USER_ID=? AND STORE_ID=10001', [$userId]) !== 0
        || $count($store, 'SELECT COUNT(*) FROM ' . $storeUser
            . ' WHERE AI_USER_ID=? AND IS_DELETE=0', [$userId]) !== 0) {
        throw new RuntimeException('Unexpected linked account row');
    }
    $result['original_relation_checked'] = true;
} catch (Throwable $error) {
    $failed = get_class($error);
    $result['failure_class'] = $failed;
} finally {
    $journalHealthy = true;
    try {
        $saveJournal(['phase' => 'cleanup_started',
            'ai_user_id' => $userId, 'ai_staff_id' => $staffId,
            'original_user_sha256' => $rowHashes['user'] ?? null,
            'original_staff_sha256' => $rowHashes['staff'] ?? null]);
    } catch (Throwable $journalError) {
        $journalHealthy = false;
        $result['cleanup'] = 'deferred_journal_failure';
        $result['private_journal_path'] = $journal;
    }
    // This is always our cryptographically random cache key, never a user's key.
    try {
        \think\facade\Cache::delete($token);
        // Never short-circuit this readback: a delete return value is not
        // proof that the key is gone from the HTTP worker's cache backend.
        $tokenCleared = !\think\facade\Cache::has($token);
    } catch (Throwable $cacheError) {
        $tokenCleared = false;
    }
    $safeToClean = $requestComplete && $tokenCleared && $journalHealthy;
    if (!$requestComplete) {
        // Transport outcome is unknown. Do not race an in-flight worker.
        // Recovery must wait until token_expires_at plus margin, then use
        // a fresh same-worker boundary barrier before exact-row cleanup.
        $safeToClean = false;
    }
    try {
    if (!$journalHealthy) {
        $result['cleanup'] = 'deferred_journal_failure';
    } elseif ($safeToClean && $userId > 0) {
        $userRows = $readRows($ai, 'SELECT * FROM ' . $aiUser
            . ' WHERE USER_ID=? AND STORE_ID=10001 AND NICK_NAME=?', [$userId, $marker]);
        $staffRows = $staffId > 0 ? $readRows($ai, 'SELECT * FROM ' . $aiStaff
            . ' WHERE ID=? AND USER_ID=? AND NAME=?', [$staffId, $userId, $marker]) : [];
        $linked = $count($ai, 'SELECT COUNT(*) FROM ' . $aiSubsidy
            . ' WHERE USER_ID=? AND STORE_ID=10001', [$userId]);
        $oauthLinked = $count($ai, 'SELECT COUNT(*) FROM ' . $aiOauth
            . ' WHERE USER_ID=?', [$userId]);
        $balanceLogLinked = $count($ai, 'SELECT COUNT(*) FROM ' . $aiBalanceLog
            . ' WHERE USER_ID=? AND STORE_ID=10001', [$userId]);
        $storeLinked = $count($store, 'SELECT COUNT(*) FROM ' . $storeUser
            . ' WHERE AI_USER_ID=?', [$userId]);
        $remainingStaff = $count($ai, 'SELECT COUNT(*) FROM ' . $aiStaff
            . ' WHERE USER_ID=? AND STORE_ID=10001 AND NAME=?', [$userId, $marker]);
        $unchanged = count($userRows) === 1
            && $fingerprint($userRows[0]) === ($rowHashes['user'] ?? null)
            && ($staffId === 0 || (count($staffRows) === 1
                && $fingerprint($staffRows[0]) === ($rowHashes['staff'] ?? null)))
            && $remainingStaff === ($staffId > 0 ? 1 : 0)
            && $linked === 0 && $oauthLinked === 0
            && $balanceLogLinked === 0 && $storeLinked === 0;
        if ($unchanged) {
            if ($staffId > 0) {
                $staffDelete = $ai->prepare('DELETE FROM ' . $aiStaff
                    . ' WHERE ID=? AND USER_ID=? AND STORE_ID=10001 AND NAME=?');
                $staffDelete->execute([$staffId, $userId, $marker]);
                if ($staffDelete->rowCount() !== 1) {
                    throw new RuntimeException('Synthetic staff delete count mismatch');
                }
            }
            $userDelete = $ai->prepare('DELETE FROM ' . $aiUser
                . ' WHERE USER_ID=? AND STORE_ID=10001 AND NICK_NAME=?');
            $userDelete->execute([$userId, $marker]);
            if ($userDelete->rowCount() !== 1) {
                throw new RuntimeException('Synthetic user delete count mismatch');
            }
            $result['cleanup'] = $count($ai, 'SELECT COUNT(*) FROM ' . $aiUser
                . ' WHERE USER_ID=? AND STORE_ID=10001', [$userId]) === 0
                && $count($ai, 'SELECT COUNT(*) FROM ' . $aiStaff
                    . ' WHERE USER_ID=? AND STORE_ID=10001', [$userId]) === 0
                ? 'exact_zero' : 'failed_readback';
            if ($result['cleanup'] === 'exact_zero') {
                $saveJournal(['phase' => 'first_process_exact_zero']);
            }
        } else {
            $result['cleanup'] = 'deferred_unknown_relation_or_row_change';
        }
    } elseif ($userId > 0) {
        $result['cleanup'] = $tokenCleared
            ? 'deferred_wait_ttl_and_same_worker_barrier'
            : 'deferred_token_cache_not_cleared_wait_ttl';
    } else {
        $result['cleanup'] = 'deferred_no_fixture_id_check_private_journal';
    }
    } catch (Throwable $cleanupError) {
        $result['cleanup'] = 'deferred_cleanup_exception';
        $result['cleanup_error_class'] = get_class($cleanupError);
    }
    $result['private_journal_path'] = $journal;
}

$result['status'] = ($failed === null && ($result['cleanup'] ?? '') === 'exact_zero')
    ? 'first_process_complete_pending_independent_zero'
    : 'first_process_failed_or_deferred';
echo json_encode($result, JSON_UNESCAPED_SLASHES), PHP_EOL;
exit($failed === null && ($result['cleanup'] ?? '') === 'exact_zero' ? 0 : 3);
