<?php

declare(strict_types=1);

// Balance-only A/B runner. Does not expose the broad subsidy backfill command.
$lane = (string)getenv('CDX_BALANCE_AB_LANE');
$mode = (string)($argv[1] ?? '');
if (count($argv) !== 2 || !in_array($lane, ['A', 'B'], true)
    || !in_array($mode, ['--preflight', '--run-balance'], true)
    || (string)getenv('CDX_BALANCE_AB_RUNNER_SHA')
        !== hash_file('sha256', __FILE__)
    || (string)getenv('CDX_BALANCE_AB_WINDOW')
        !== 'ROOT_CONFIRMED_NO_PARALLEL_DEV_MYSQL_V1'
    || ($mode === '--preflight'
        && (string)getenv('CDX_BALANCE_AB_RUNNER_PREFLIGHT')
            !== 'ROOT_APPROVED_BALANCE_ONLY_SELECT_V1')
    || ($mode === '--run-balance'
        && (string)getenv('CDX_BALANCE_AB_EXECUTE')
            !== 'ROOT_APPROVED_BALANCE_CHECK_ONLY_V1')) {
    echo json_encode(['status' => 'default-deny',
        'db_connected' => false, 'cli_started' => false,
        'business_http_sent' => false, 'dml_sent' => false]), PHP_EOL;
    exit(78);
}
if (PHP_VERSION_ID < 70400 || PHP_VERSION_ID >= 70500
    || realpath(PHP_BINARY)
        !== '/opt/homebrew/Cellar/php@7.4/7.4.33_13/bin/php') {
    throw new RuntimeException('Approved local PHP7.4 interpreter differs');
}
$roots = [
    'A' => ['/private/tmp/zhct-http-ai-macos.zjaU8h', 13109,
        'e7402b1ad50fcf4205326d93dc22e4f491c5052c60fcbe36214cd4e07b55343c'],
    'B' => ['/private/tmp/zhct-http-ai-macos.x2CCZX', 13127,
        '8c46a6d9cd277f2950818d09162f639a62f135a5eb6553e6c61ce2c19167fd02'],
];
[$expectedRoot, $expectedPid, $configSha] = $roots[$lane];
$preflightFile = __DIR__ . '/preflight_ab_balance_only.php';
if (is_link($expectedRoot) || !is_file($preflightFile)
    || hash_file('sha256', $expectedRoot . '/think-poc')
        !== 'cbd8c0b81a4ffe64b68ed931bf1bbb08bbe5e7537f991d45c0be018d48e6e5cb'
    || hash_file('sha256', $expectedRoot . '/config/database.php')
        !== $configSha
    || hash_file('sha256', $preflightFile)
        !== '53dc60b463faf7418aabf43aea023c13663d9b1ac036e2da1935433684fa74c0') {
    throw new RuntimeException('A/B balance-only CLI root/preflight drift');
}
$lockPath = '/tmp/zhct-ai-balance-only-ab.lock';
if (is_link($lockPath) || (file_exists($lockPath) && !is_file($lockPath))) {
    throw new RuntimeException('A/B balance-only lock target unsafe');
}
$lock = fopen($lockPath, 'c');
if (!is_resource($lock) || !flock($lock, LOCK_EX | LOCK_NB)) {
    throw new RuntimeException('Shared A/B balance-only lock busy');
}
chmod($lockPath, 0600);
putenv('CDX_BALANCE_AB_LANE=' . $lane);
putenv('CDX_BALANCE_AB_PREFLIGHT=ROOT_APPROVED_BALANCE_SELECT_V1');
putenv('CDX_BALANCE_AB_PREFLIGHT_SHA=' . hash_file('sha256', $preflightFile));
$outerArgv = $argv;
$argv[1] = '--preflight-only';
ob_start();
require $preflightFile;
$preflightOutput = (string)ob_get_clean();
$argv = $outerArgv;
$preflight = json_decode(trim($preflightOutput), true);
if (!is_array($preflight)
    || ($preflight['status'] ?? null) !== 'balance-only-preflight'
    || ($preflight['lane'] ?? null) !== $lane
    || ($preflight['developer_ai_db_fingerprint'] ?? null)
        !== '210f925e093f71e26a8c8923f0f626e3be0a15ccff3d386bba21feef695c8a5b'
    || $root !== $expectedRoot || $pid !== $expectedPid
    || $balanceTotal > 100000 || !is_array($selected)) {
    throw new RuntimeException('A/B balance-only SELECT source gate failed');
}
$sourceKey = hash('sha256', json_encode([
    $lane, $selected, $balanceTotal, $subsidyTotal,
], JSON_UNESCAPED_SLASHES));
if ($mode === '--preflight') {
    echo json_encode(['status' => 'balance-runner-preflight-only',
        'lane' => $lane, 'source_key' => $sourceKey,
        'balance_log_total_rows' => $balanceTotal,
        'subsidy_log_total_count_only' => $subsidyTotal,
        'approved_synthetic_source' => $selected,
        'cli_started' => false, 'dml_sent' => false],
        JSON_UNESCAPED_SLASHES), PHP_EOL;
    exit(0);
}
if ((string)getenv('CDX_BALANCE_AB_SOURCE_KEY') !== $sourceKey
    || !preg_match('/^CDXPAY[A-F0-9]{8}$/D',
        (string)($selected['marker'] ?? ''))
    || (int)$selected['store_id'] !== 10001
    || (int)$selected['user_id'] <= 0
    || (int)$selected['cutoff_log_id'] <= 0
    || (int)$selected['owned_log_rows'] < 1
    || (int)$selected['owned_log_rows'] > 200) {
    throw new RuntimeException('A/B source key or synthetic user not approved');
}
$userId = (int)$selected['user_id'];
$storeId = (int)$selected['store_id'];
$cutoffId = (int)$selected['cutoff_log_id'];
$process = (string)shell_exec('/bin/ps eww -p ' . $expectedPid . ' -o command=');
$utf8mb4 = preg_match('/(?:^|\s)POC_MYSQL_UTF8MB4=1(?=\s|$)/', $process) === 1;
$childEnv = ['PATH' => '/usr/bin:/bin', 'LANG' => 'C.UTF-8',
    'POC_DIALECT' => 'mysql', 'POC_EXTERNAL_STUBS' => '1',
    'POC_MYSQL_UTF8MB4' => $utf8mb4 ? '1' : '0'];
foreach (['POC_AI_HOST', 'POC_AI_PORT', 'POC_AI_SCHEMA', 'POC_AI_USER',
    'POC_AI_PASSWORD', 'POC_HOST', 'POC_PORT', 'POC_SCHEMA',
    'POC_USER', 'POC_PASSWORD'] as $key) {
    if (empty($config[$key])) throw new RuntimeException('Approved CLI env incomplete');
    $childEnv[$key] = $config[$key];
}
$command = [PHP_BINARY, $root . '/think-poc',
    'initBalanceLogFields', '--check-only',
    '--store-id=' . $storeId, '--user-id=' . $userId,
    '--cutoff-log-id=' . $cutoffId];
$runExact = static function (array $fixedCommand, array $env, string $cwd): array {
    $proc = proc_open($fixedCommand,
        [0 => ['pipe', 'r'], 1 => ['pipe', 'w'], 2 => ['pipe', 'w']],
        $pipes, $cwd, $env, ['bypass_shell' => true]);
    if (!is_resource($proc)) throw new RuntimeException('Balance-only CLI could not start');
    fclose($pipes[0]);
    stream_set_blocking($pipes[1], false);
    stream_set_blocking($pipes[2], false);
    $stdout = '';
    $stderr = '';
    $exitCode = -1;
    $timedOut = false;
    $deadline = microtime(true) + 25.0;
    try {
        while (true) {
            $stdout .= (string)stream_get_contents($pipes[1]);
            $stderr .= (string)stream_get_contents($pipes[2]);
            if (strlen($stdout) > 8192 || strlen($stderr) > 8192) {
                throw new RuntimeException('Balance-only CLI output exceeded bound');
            }
            $status = proc_get_status($proc);
            if (!$status['running']) {
                $exitCode = (int)$status['exitcode'];
                break;
            }
            if (microtime(true) > $deadline) {
                $timedOut = true;
                proc_terminate($proc);
                throw new RuntimeException('Balance-only CLI timeout; no retry');
            }
            usleep(100000);
        }
    } finally {
        $last = proc_get_status($proc);
        if ($last['running']) proc_terminate($proc, 9);
        if (is_resource($pipes[1])) fclose($pipes[1]);
        if (is_resource($pipes[2])) fclose($pipes[2]);
        $close = proc_close($proc);
        if ($exitCode < 0 && !$timedOut) $exitCode = (int)$close;
    }
    if ($exitCode !== 0 || $stderr !== '') {
        throw new RuntimeException('Balance-only CLI error exit=' . $exitCode
            . ' stderr_present=' . ($stderr === '' ? 'no' : 'yes')
            . ' stderr_sha=' . hash('sha256', $stderr));
    }
    return array_values(array_filter(array_map('trim',
        preg_split('/\r?\n/', $stdout)), static function (string $line): bool {
        return $line !== '';
    }));
};
$lines = $runExact($command, $childEnv, $root);
if ($lines !== ['missing_count=' . (int)$selected['expected_missing_count']]) {
    throw new RuntimeException('Balance-only CLI output differs from own source');
}
$after = new PDO($dsn, $config['POC_AI_USER'], $config['POC_AI_PASSWORD'], [
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_CASE => PDO::CASE_LOWER,
    PDO::ATTR_TIMEOUT => 5,
]);
$afterIdentity = (string)$after->query(
    "SELECT CONCAT(@@server_uuid,'|',@@hostname,'|',@@port,'|',DATABASE())")
    ->fetchColumn();
if (hash('sha256', $afterIdentity)
    !== '210f925e093f71e26a8c8923f0f626e3be0a15ccff3d386bba21feef695c8a5b') {
    throw new RuntimeException('Post-CLI developer AI MySQL identity changed');
}
$userStmt = $after->prepare('SELECT USER_ID,STORE_ID,NICK_NAME,BALANCE,'
    . ' SUBSIDY_BALANCE FROM yoshop_user WHERE USER_ID=? AND STORE_ID=?');
$userStmt->execute([$userId, $storeId]);
$userRows = $userStmt->fetchAll(PDO::FETCH_ASSOC);
if (count($userRows) !== 1
    || hash('sha256', serialize($userRows[0])) !== $selected['user_digest']) {
    throw new RuntimeException('Post-CLI synthetic user changed');
}
$logsStmt = $after->prepare('SELECT LOG_ID,USER_ID,STORE_ID,MONEY,BALANCE_AFTER'
    . ' FROM yoshop_user_balance_log WHERE USER_ID=? AND STORE_ID=?'
    . ' ORDER BY LOG_ID DESC LIMIT 201');
$logsStmt->execute([$userId, $storeId]);
$logs = $logsStmt->fetchAll(PDO::FETCH_ASSOC);
if (count($logs) !== (int)$selected['owned_log_rows']
    || $digestRows($logs) !== $selected['balance_log_digest']) {
    throw new RuntimeException('Post-CLI synthetic balance log changed');
}
$missingStmt = $after->prepare('SELECT COUNT(*)'
    . ' FROM yoshop_user_balance_log WHERE BALANCE_AFTER IS NULL'
    . ' AND STORE_ID=? AND USER_ID=? AND LOG_ID<=?');
$missingStmt->execute([$storeId, $userId, $cutoffId]);
if ((int)$missingStmt->fetchColumn()
    !== (int)$selected['expected_missing_count']) {
    throw new RuntimeException('Post-CLI balance missing count changed');
}
$afterBalanceTotal = (int)$after->query(
    'SELECT COUNT(*) FROM yoshop_user_balance_log')->fetchColumn();
$afterSubsidyTotal = (int)$after->query(
    'SELECT COUNT(*) FROM yoshop_user_subsidy_log')->fetchColumn();
if ($afterBalanceTotal !== $balanceTotal
    || $afterSubsidyTotal !== $subsidyTotal) {
    throw new RuntimeException('Post-CLI business log table count changed');
}
echo json_encode(['status' => 'balance-check-only-cli-passed',
    'lane' => $lane, 'strict_command_argv' => true,
    'output_fields' => $lines, 'approved_dev_mysql_unchanged' => true,
    'synthetic_user_and_balance_log_digest_unchanged' => true,
    'subsidy_log_total_count_unchanged' => true,
    'business_http_sent' => false, 'business_dml' => false],
    JSON_UNESCAPED_SLASHES), PHP_EOL;
