<?php

declare(strict_types=1);

// A/B developer-MySQL-only. No command or DB access without explicit review.
$lane = (string)getenv('CDX_AI_LOG_AB_LANE');
$mode = (string)($argv[1] ?? '');
$modes = ['--preflight' => null,
    '--run-balance' => 'balance', '--run-subsidy' => 'subsidy'];
if (count($argv) !== 2 || !in_array($lane, ['A', 'B'], true)
    || !array_key_exists($mode, $modes)
    || (string)getenv('CDX_AI_LOG_AB_SHA') !== hash_file('sha256', __FILE__)
    || (string)getenv('CDX_AI_LOG_AB_WINDOW')
        !== 'ROOT_CONFIRMED_NO_PARALLEL_DEV_MYSQL_V1'
    || ($mode === '--preflight'
        && (string)getenv('CDX_AI_LOG_AB_PREFLIGHT')
            !== 'ROOT_APPROVED_AB_READONLY_PREFLIGHT_V1')
    || ($mode !== '--preflight'
        && ((string)getenv('CDX_AI_LOG_AB_EXECUTE')
                !== 'ROOT_APPROVED_AB_CHECK_ONLY_V1'))) {
    echo json_encode(['status' => 'default-deny', 'cli_started' => false,
        'db_connected' => 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 CLI differs');
}
$approvedRoots = [
    'A' => ['/private/tmp/zhct-http-ai-macos.zjaU8h', 18105, 13109,
        'e7402b1ad50fcf4205326d93dc22e4f491c5052c60fcbe36214cd4e07b55343c'],
    'B' => ['/private/tmp/zhct-http-ai-macos.x2CCZX', 18101, 13127,
        '8c46a6d9cd277f2950818d09162f639a62f135a5eb6553e6c61ce2c19167fd02'],
];
[$expectedRoot, $expectedPort, $expectedPid, $configSha] = $approvedRoots[$lane];
$preflightFile = __DIR__ . '/preflight_log_commands.php';
if (is_link($expectedRoot) || !is_file($expectedRoot . '/think-poc')
    || !is_file($expectedRoot . '/config/database.php')
    || !is_file($preflightFile)
    || hash_file('sha256', $expectedRoot . '/think-poc')
        !== 'cbd8c0b81a4ffe64b68ed931bf1bbb08bbe5e7537f991d45c0be018d48e6e5cb'
    || hash_file('sha256', $expectedRoot . '/config/database.php')
        !== $configSha
    || hash_file('sha256', $preflightFile)
        !== '775432cec6190bdec5313675cece87d1c26c57144e2d60b236cceaadca57ffde') {
    throw new RuntimeException('Approved local CLI root/preflight/source drift');
}
$lockPath = '/tmp/zhct-ai-log-command-ab.lock';
if (is_link($lockPath) || (file_exists($lockPath) && !is_file($lockPath))) {
    throw new RuntimeException('A/B shared lock target unsafe');
}
$lock = fopen($lockPath, 'c');
if (!is_resource($lock) || !flock($lock, LOCK_EX | LOCK_NB)) {
    throw new RuntimeException('A/B shared developer MySQL command lock busy');
}
chmod($lockPath, 0600);
// Existing SELECT-only gate validates the fixed listener root/PID, code
// .env.local connection values, DB identity, table limits and source digests.
putenv('CDX_AI_LOG_LANE=' . $lane);
putenv('CDX_AI_LOG_PREFLIGHT=READ_ONLY_TWO_COMMANDS_V1');
$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) !== 'preflight-only'
    || ($preflight['lane'] ?? null) !== $lane
    || ($preflight['database_fingerprint'] ?? null)
        !== '210f925e093f71e26a8c8923f0f626e3be0a15ccff3d386bba21feef695c8a5b'
    || $root !== $expectedRoot || $httpPort !== $expectedPort
    || $httpPid !== $expectedPid
    || $subsidyTotal > 10000) {
    throw new RuntimeException('A/B SELECT-only source/target gate failed');
}
$sourceKey = hash('sha256', json_encode([
    $lane, $selected, $balanceTotal, $subsidyTotal,
    $subsidyMissingBalance, $subsidyMissingOperator,
    $subsidySourceDigest,
], JSON_UNESCAPED_SLASHES));
if ($mode === '--preflight') {
    echo json_encode(['status' => 'runner-preflight-only', 'lane' => $lane,
        'cli_started' => false, 'approved_source_key' => $sourceKey,
        'balance_source' => $selected,
        'subsidy_expected' => $preflight['subsidy_check_only_expected'],
        'business_http_sent' => false, 'dml_sent' => false],
        JSON_UNESCAPED_SLASHES), PHP_EOL;
    exit(0);
}
if ((string)getenv('CDX_AI_LOG_AB_SOURCE_KEY') !== $sourceKey) {
    throw new RuntimeException('A/B frozen source key not approved');
}
if ($modes[$mode] === 'balance'
    && (!is_array($selected)
        || !preg_match('/^CDXPAY[A-F0-9]{8}$/D',
            (string)($selected['marker'] ?? ''))
        || (int)($selected['store_id'] ?? 0) !== 10001
        || (int)($selected['user_id'] ?? 0) <= 0
        || (int)($selected['cutoff_log_id'] ?? 0) <= 0
        || (int)($selected['owned_log_rows'] ?? 0) < 1
        || (int)($selected['owned_log_rows'] ?? 0) > 200)) {
    throw new RuntimeException('A/B exact synthetic balance source unavailable');
}

$userStmt = $db->prepare('SELECT USER_ID,STORE_ID,NICK_NAME,BALANCE,'
    . ' SUBSIDY_BALANCE FROM yoshop_user WHERE USER_ID=? AND STORE_ID=?');
$sourceUserId = (int)($selected['user_id'] ?? 0);
$sourceStoreId = (int)($selected['store_id'] ?? 0);
if ($sourceUserId > 0) {
    $userStmt->execute([$sourceUserId, $sourceStoreId]);
    $userBefore = $userStmt->fetchAll(PDO::FETCH_ASSOC);
    if (count($userBefore) !== 1
        || (string)($userBefore[0]['nick_name'] ?? '')
            !== (string)$selected['marker']) {
        throw new RuntimeException('A/B selected synthetic user drift');
    }
    $userSha = hash('sha256', serialize($userBefore[0]));
} else {
    $userSha = null;
}
$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 local CLI env incomplete');
    $childEnv[$key] = $config[$key];
}
$command = [PHP_BINARY, $root . '/think-poc',
    $modes[$mode] === 'balance' ? 'initBalanceLogFields' : 'initSubsidyLogFields',
    '--check-only'];
if ($modes[$mode] === 'balance') {
    array_push($command,
        '--store-id=' . $sourceStoreId,
        '--user-id=' . $sourceUserId,
        '--cutoff-log-id=' . (int)$selected['cutoff_log_id']);
}
$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('Local check-only CLI could not start');
    fclose($pipes[0]);
    stream_set_blocking($pipes[1], false);
    stream_set_blocking($pipes[2], false);
    $stdout = '';
    $stderr = '';
    $exitCode = -1;
    $deadline = microtime(true) + 25.0;
    $timedOut = false;
    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('Local CLI output exceeds 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('Local CLI timeout; no automatic 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('Local 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);
$expectedLines = $modes[$mode] === 'balance'
    ? ['missing_count=' . (int)$selected['expected_missing_count']]
    : ['total_count=' . $subsidyTotal,
        'missing_balance_count=' . $subsidyMissingBalance,
        'missing_operator_count=' . $subsidyMissingOperator];
if ($lines !== $expectedLines) {
    throw new RuntimeException('A/B CLI output differs from own source counts');
}

$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 MySQL identity drift');
}
if ($sourceUserId > 0) {
    $afterUserStmt = $after->prepare('SELECT USER_ID,STORE_ID,NICK_NAME,BALANCE,'
        . ' SUBSIDY_BALANCE FROM yoshop_user WHERE USER_ID=? AND STORE_ID=?');
    $afterUserStmt->execute([$sourceUserId, $sourceStoreId]);
    $userAfter = $afterUserStmt->fetchAll(PDO::FETCH_ASSOC);
    if (count($userAfter) !== 1
        || hash('sha256', serialize($userAfter[0])) !== $userSha) {
        throw new RuntimeException('A/B synthetic user changed during check-only');
    }
    $balanceStmt = $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');
    $balanceStmt->execute([$sourceUserId, $sourceStoreId]);
    $balanceRows = $balanceStmt->fetchAll(PDO::FETCH_ASSOC);
    if (count($balanceRows) !== (int)$selected['owned_log_rows']
        || $digestRows($balanceRows) !== $selected['source_digest']) {
        throw new RuntimeException('A/B synthetic balance log digest 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([$sourceStoreId, $sourceUserId,
        (int)$selected['cutoff_log_id']]);
    if ((int)$missingStmt->fetchColumn()
        !== (int)$selected['expected_missing_count']) {
        throw new RuntimeException('A/B balance missing count changed');
    }
}
$counts = [
    (int)$after->query('SELECT COUNT(*) FROM yoshop_user_subsidy_log')
        ->fetchColumn(),
    (int)$after->query('SELECT COUNT(*) FROM yoshop_user_subsidy_log'
        . ' WHERE SUBSIDY_BALANCE_AFTER IS NULL')->fetchColumn(),
    (int)$after->query('SELECT COUNT(*) FROM yoshop_user_subsidy_log'
        . " WHERE OPERATOR_NAME=''")->fetchColumn(),
];
if ($counts !== [$subsidyTotal, $subsidyMissingBalance,
    $subsidyMissingOperator]) {
    throw new RuntimeException('A/B subsidy check-only counts changed');
}
$subsidyRows = $after->query('SELECT LOG_ID,USER_ID,MONEY,'
    . ' SUBSIDY_BALANCE_AFTER,OPERATOR_NAME FROM yoshop_user_subsidy_log'
    . ' ORDER BY LOG_ID ASC LIMIT 10001')->fetchAll(PDO::FETCH_ASSOC);
if (count($subsidyRows) !== $subsidyTotal
    || $digestRows($subsidyRows) !== $subsidySourceDigest) {
    throw new RuntimeException('A/B subsidy source digest changed');
}
echo json_encode(['status' => 'check-only-cli-passed', 'lane' => $lane,
    'mode' => $modes[$mode], 'strict_command_argv' => true,
    'output_fields' => $lines, 'developer_mysql_identity_unchanged' => true,
    'synthetic_user_and_balance_logs_unchanged' => true,
    'subsidy_log_count_and_digest_unchanged' => true,
    'external_http_sent' => false, 'business_dml' => false],
    JSON_UNESCAPED_SLASHES), PHP_EOL;
