<?php

declare(strict_types=1);

// SELECT-only gate for two registered AI CLI --check-only branches.
// This file never invokes think, the command classes, DML, or DDL.
$stage = 'default-deny';
set_exception_handler(static function (Throwable $e) use (&$stage): void {
    echo json_encode(['status' => 'redacted-error', 'stage' => $stage,
        'error_class' => get_class($e), 'error_code' => (string)$e->getCode()],
        JSON_UNESCAPED_SLASHES), PHP_EOL;
    exit(2);
});
set_error_handler(static function (int $severity): bool {
    if (!(error_reporting() & $severity)) return false;
    throw new ErrorException('redacted warning', 0, $severity);
});
$lane = (string)getenv('CDX_AI_LOG_LANE');
if (!in_array($lane, ['A', 'B', 'C'], true)
    || ($argv[1] ?? '') !== '--preflight-only'
    || (string)getenv('CDX_AI_LOG_PREFLIGHT') !== 'READ_ONLY_TWO_COMMANDS_V1') {
    echo json_encode(['status' => 'default-deny',
        'database_connected' => false, 'think_command_invoked' => false,
        'business_http_sent' => false, 'dml_sent' => false],
        JSON_UNESCAPED_SLASHES), PHP_EOL;
    exit(78);
}
if (PHP_VERSION_ID < 70400 || PHP_VERSION_ID >= 70500) {
    throw new RuntimeException('CLI PHP version differs from target 7.4');
}
$stage = 'runtime-source';
$targets = [
    'A' => ['/private/tmp/zhct-http-ai-macos.zjaU8h', 18105, 13109, 'mysql'],
    'B' => ['/private/tmp/zhct-http-ai-macos.x2CCZX', 18101, 13127, 'mysql'],
    'C' => ['/tmp/zhct-ai-http-20260929.3XDj3w', 18102, 53772, 'dm'],
];
[$root, $httpPort, $httpPid, $dialect] = $targets[$lane];
if (is_link($root) || !is_file($root . '/app/command/InitBalanceLogFields.php')
    || !is_file($root . '/app/command/InitSubsidyLogFields.php')
    || !is_file($root . '/config/console.php')) {
    throw new RuntimeException('AI isolated CLI root unavailable');
}
$expectedSources = [
    'InitBalanceLogFields.php'
        => 'de886446344435f7a819105788ffc699c71a959119877b4b2e5de7dbe37f0b62',
    'InitSubsidyLogFields.php'
        => '3322b8fbd35c93a6ae49a72d97bbef5c8adaa1154809aa1f8913bd5eb61f07da',
];
foreach ($expectedSources as $file => $sha) {
    if (!hash_equals($sha,
        hash_file('sha256', $root . '/app/command/' . $file))) {
        throw new RuntimeException('AI command source SHA differs');
    }
}
$registry = (string)file_get_contents($root . '/config/console.php');
foreach (['InitBalanceLogFields', 'InitSubsidyLogFields'] as $class) {
    if (strpos($registry, 'app\\command\\' . $class) === false) {
        throw new RuntimeException('AI CLI command not registered');
    }
}
$config = [];
$stage = 'approved-cli-runtime';
if ($lane === 'C') {
    $cmdline = (string)file_get_contents('/proc/' . $httpPid . '/cmdline');
    $raw = (string)file_get_contents('/proc/' . $httpPid . '/environ');
    if (strpos($cmdline, '127.0.0.1:' . $httpPort) === false
        || strpos($cmdline, $root . '/public') === false
        || $raw === '' || strpos($raw, 'PHP_CLI_SERVER_WORKERS=') !== false) {
        throw new RuntimeException('C CLI/HTTP runtime identity mismatch');
    }
    foreach (explode("\0", $raw) as $entry) {
        if (strpos($entry, 'POC_AI_') !== 0) continue;
        $at = strpos($entry, '=');
        if ($at !== false) $config[substr($entry, 0, $at)] = substr($entry, $at + 1);
    }
    foreach (['POC_AI_HOST' => '127.0.0.1', 'POC_AI_PORT' => '5238',
        'POC_AI_SCHEMA' => 'AIZHCT_APP', 'POC_AI_USER' => 'AIZHCT_APP']
        as $key => $expected) {
        if (($config[$key] ?? '') !== $expected) {
            throw new RuntimeException('C AI DB target differs');
        }
    }
    $machineId = trim((string)file_get_contents('/etc/machine-id'));
    if ($machineId === '' || gethostname() === false
        || hash('sha256', gethostname() . '|' . $machineId)
            !== 'd56d81988bb0f106946cce91c608fdd0ef1116e5150b23376ea87e086eace4b9') {
        throw new RuntimeException('C host identity mismatch');
    }
} else {
    $pids = array_values(array_filter(explode("\n", trim((string)shell_exec(
        '/usr/sbin/lsof -t -nP -iTCP:' . $httpPort
        . ' -sTCP:LISTEN 2>/dev/null'))),
        static function (string $v): bool { return ctype_digit($v); }));
    if (count($pids) !== 1 || (int)$pids[0] !== $httpPid) {
        throw new RuntimeException('A/B isolated CLI listener PID drift');
    }
    $cmdline = (string)shell_exec('/bin/ps eww -p ' . $httpPid . ' -o command=');
    if (strpos($cmdline, '-S 127.0.0.1:' . $httpPort) === false
        || (strpos($cmdline, $root . '/public') === false
            && strpos($cmdline, str_replace('/private/tmp/', '/tmp/', $root)
                . '/public') === false)
        || strpos($cmdline, 'PHP_CLI_SERVER_WORKERS=') !== false) {
        throw new RuntimeException('A/B HTTP root/process mismatch');
    }
    $localEnv = '/Users/wiscourper/Documents/workspace/zhct/zhctproject/ai_api/.env.local';
    if (realpath($localEnv) !== $localEnv) {
        throw new RuntimeException('Code-local AI config unavailable');
    }
    $ini = parse_ini_file($localEnv, true, INI_SCANNER_RAW);
    if (!is_array($ini) || !isset($ini['DATABASE'])) {
        throw new RuntimeException('Code-local AI DATABASE section absent');
    }
    foreach (['HOSTNAME' => 'HOST', 'HOSTPORT' => 'PORT',
        'DATABASE' => 'SCHEMA', 'USERNAME' => 'USER',
        'PASSWORD' => 'PASSWORD'] as $field => $suffix) {
        $key = 'POC_AI_' . $suffix;
        $value = (string)($ini['DATABASE'][$field] ?? '');
        if ($value === '' || preg_match('/(?:^|\s)'
            . preg_quote($key . '=' . $value, '/') . '(?=\s|$)/', $cmdline) !== 1) {
            throw new RuntimeException('A/B AI runtime differs from code local');
        }
        $config[$key] = $value;
    }
}
foreach (['HOST', 'PORT', 'SCHEMA', 'USER', 'PASSWORD'] as $suffix) {
    if (empty($config['POC_AI_' . $suffix])) {
        throw new RuntimeException('Approved AI DB setting unavailable');
    }
}
$boundary = json_decode((string)file_get_contents(
    'http://127.0.0.1:' . $httpPort . '/__poc_boundary', false,
    stream_context_create(['http' => ['timeout' => 8,
        'follow_location' => 0, 'ignore_errors' => true]])), true);
if (!is_array($boundary) || ($boundary['external_stubs'] ?? null) !== true) {
    throw new RuntimeException('AI isolated external-stub boundary absent');
}

$stage = 'approved-database';
$dsn = $dialect === 'dm'
    ? 'dm:host=127.0.0.1;port=5238;schema=AIZHCT_APP'
    : 'mysql:host=' . $config['POC_AI_HOST'] . ';port=' . $config['POC_AI_PORT']
        . ';dbname=' . $config['POC_AI_SCHEMA'] . ';charset=utf8';
try {
    $db = 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,
    ]);
} catch (Throwable $e) {
    throw new RuntimeException('Approved AI DB connection failed: ' . get_class($e));
}
$stage = 'database-identity';
$identity = $dialect === 'dm'
    ? 'AIZHCT_APP|5238|' . (string)$db->query(
        "SELECT SYS_CONTEXT('USERENV','CURRENT_SCHEMA') FROM DUAL")->fetchColumn()
    : (string)$db->query(
        "SELECT CONCAT(@@server_uuid,'|',@@hostname,'|',@@port,'|',DATABASE())")
        ->fetchColumn();
$approvedIdentity = $dialect === 'dm'
    ? '628c12663da929e593de74c9c58e5bcfab0530fbfbcb1d90a9ed7b736a45fd5e'
    : '210f925e093f71e26a8c8923f0f626e3be0a15ccff3d386bba21feef695c8a5b';
if (!hash_equals($approvedIdentity, hash('sha256', $identity))) {
    throw new RuntimeException('AI DB identity differs from approved nonproduction target');
}
$prefix = $dialect === 'dm' ? 'YOSHOP_' : 'yoshop_';
$balanceTable = $prefix . ($dialect === 'dm' ? 'USER_BALANCE_LOG' : 'user_balance_log');
$subsidyTable = $prefix . ($dialect === 'dm' ? 'USER_SUBSIDY_LOG' : 'user_subsidy_log');
$userTable = $prefix . ($dialect === 'dm' ? 'USER' : 'user');
$stage = 'schema-balance-log';
$db->query('SELECT LOG_ID,USER_ID,STORE_ID,MONEY,BALANCE_AFTER FROM '
    . $balanceTable . ' WHERE 1=0');
$stage = 'schema-subsidy-log';
$db->query('SELECT LOG_ID,USER_ID,MONEY,SUBSIDY_BALANCE_AFTER,OPERATOR_NAME'
    . ' FROM ' . $subsidyTable . ' WHERE 1=0');
$stage = 'schema-ai-user';
$db->query('SELECT USER_ID,STORE_ID,NICK_NAME,IS_DELETE FROM '
    . $userTable . ' WHERE 1=0');

$stage = 'bounded-source-counts';
$balanceTotal = (int)$db->query('SELECT COUNT(*) FROM ' . $balanceTable)->fetchColumn();
$subsidyTotal = (int)$db->query('SELECT COUNT(*) FROM ' . $subsidyTable)->fetchColumn();
if ($balanceTotal < 0 || $balanceTotal > 100000
    || $subsidyTotal < 0 || $subsidyTotal > 10000) {
    throw new RuntimeException('Log table size exceeds read-only gate');
}
$subsidyMissingBalance = (int)$db->query('SELECT COUNT(*) FROM '
    . $subsidyTable . ' WHERE SUBSIDY_BALANCE_AFTER IS NULL')->fetchColumn();
$subsidyMissingOperator = (int)$db->query('SELECT COUNT(*) FROM '
    . $subsidyTable . " WHERE OPERATOR_NAME=''")->fetchColumn();

$sourceRows = $db->query('SELECT LOG_ID,USER_ID,MONEY,'
    . ' SUBSIDY_BALANCE_AFTER,OPERATOR_NAME FROM ' . $subsidyTable
    . ' ORDER BY LOG_ID ASC'
    . ($dialect === 'dm' ? ' FETCH FIRST 10001 ROWS ONLY' : ' LIMIT 10001'))
    ->fetchAll(PDO::FETCH_ASSOC);
if (count($sourceRows) !== $subsidyTotal) {
    throw new RuntimeException('Subsidy log count changed during source digest');
}
$asString = static function ($value): string {
    return is_resource($value) ? (string)stream_get_contents($value) : (string)$value;
};
$digestRows = static function (array $rows) use ($asString): string {
    $hash = hash_init('sha256');
    foreach ($rows as $row) {
        hash_update($hash, json_encode(array_map($asString, $row),
            JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE) . "\n");
    }
    return hash_final($hash);
};
$subsidySourceDigest = $digestRows($sourceRows);
unset($sourceRows);

$stage = 'synthetic-balance-source';
$candidateSql = 'SELECT USER_ID,STORE_ID,NICK_NAME FROM ' . $userTable
    . " WHERE NICK_NAME LIKE 'CDXPAY%' AND IS_DELETE=0 ORDER BY USER_ID DESC"
    . ($dialect === 'dm' ? ' FETCH FIRST 50 ROWS ONLY' : ' LIMIT 50');
$candidateUsers = $db->query($candidateSql)->fetchAll(PDO::FETCH_ASSOC);
$selected = null;
$uniqueSynthetic = $db->prepare('SELECT COUNT(*) FROM ' . $userTable
    . ' WHERE NICK_NAME=? AND STORE_ID=? AND IS_DELETE=0');
$candidateLogQuery = $db->prepare('SELECT LOG_ID,USER_ID,STORE_ID,MONEY,BALANCE_AFTER'
    . ' FROM ' . $balanceTable . ' WHERE USER_ID=? AND STORE_ID=?'
    . ' ORDER BY LOG_ID DESC'
    . ($dialect === 'dm' ? ' FETCH FIRST 201 ROWS ONLY' : ' LIMIT 201'));
foreach ($candidateUsers as $user) {
    $marker = (string)$user['nick_name'];
    if (!preg_match('/^CDXPAY[A-F0-9]{8}$/D', $marker)
        || (int)$user['user_id'] <= 0 || (int)$user['store_id'] <= 0) continue;
    $uniqueSynthetic->execute([$marker, (int)$user['store_id']]);
    if ((int)$uniqueSynthetic->fetchColumn() !== 1) continue;
    $candidateLogQuery->execute([(int)$user['user_id'], (int)$user['store_id']]);
    $logs = $candidateLogQuery->fetchAll(PDO::FETCH_ASSOC);
    if (!$logs || count($logs) > 200) continue;
    $maxLogId = (int)$logs[0]['log_id'];
    if ($maxLogId <= 0) continue;
    $countSql = $db->prepare('SELECT COUNT(*) FROM ' . $balanceTable
        . ' WHERE BALANCE_AFTER IS NULL AND STORE_ID=? AND USER_ID=?'
        . ' AND LOG_ID<=?');
    $countSql->execute([(int)$user['store_id'], (int)$user['user_id'], $maxLogId]);
    $selected = [
        'marker' => $marker, 'store_id' => (int)$user['store_id'],
        'user_id' => (int)$user['user_id'], 'cutoff_log_id' => $maxLogId,
        'owned_log_rows' => count($logs),
        'expected_missing_count' => (int)$countSql->fetchColumn(),
        'source_digest' => $digestRows($logs),
    ];
    break;
}
echo json_encode([
    'status' => 'preflight-only', 'lane' => $lane,
    'command_sources_sha_locked' => true,
    'cli_registered' => ['initBalanceLogFields', 'initSubsidyLogFields'],
    'database_fingerprint' => hash('sha256', $identity),
    'database_connected' => true, 'think_command_invoked' => false,
    'business_http_sent' => false, 'dml_sent' => false,
    'balance_log_total_rows' => $balanceTotal,
    'balance_check_only_source' => $selected,
    'subsidy_check_only_expected' => [
        'total_count' => $subsidyTotal,
        'missing_balance_count' => $subsidyMissingBalance,
        'missing_operator_count' => $subsidyMissingOperator,
        'source_digest' => $subsidySourceDigest,
    ],
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL;
