<?php
declare(strict_types=1);

// REVIEW-ONLY by default. After root review, one unique synthetic guest fixture
// exercises only three read endpoints, then precisely removes its two rows.
set_exception_handler(static function (Throwable $error): void {
    echo json_encode(['status' => 'runner-error', 'class' => get_class($error),
        'code' => (string)$error->getCode()], JSON_UNESCAPED_SLASHES), PHP_EOL;
    exit(2);
});
$preflight = in_array('--preflight', $argv ?? [], true);
if (!$preflight && getenv('CDX_CHAT_EXECUTE') !== 'ROOT_REVIEWED_CHAT_GUEST_V1') {
    echo json_encode(['status' => 'DENIED', 'reason' => 'root-review-required']), PHP_EOL;
    exit(0);
}
$selfSha = (string)getenv('CDX_CHAT_SCRIPT_SHA');
$lane = (string)getenv('CDX_CHAT_LANE');
$marker = (string)getenv('CDX_CHAT_MARKER');
if ($selfSha === '' || hash_file('sha256', __FILE__) !== $selfSha
    || !in_array($lane, ['A', 'B', 'C'], true)
    || !preg_match('/^guest_cdx[a-f0-9]{16}$/', $marker)) {
    throw new RuntimeException('Exact script/lane/guest marker gate failed');
}
$suffix = substr($marker, strlen('guest_cdx'));
$botId = 'cdxchatbot';
$conversationId = 'cdxchat_' . $suffix;
$contentText = '本次兼容测试会话内容';
$content = json_encode([['role' => 'user', 'content' => $contentText]], JSON_UNESCAPED_UNICODE);
$sourceSha = '4431e6451ac088ad325a23626ee669996dc8fde1e92e721b1eccad085bd3e2fc';
$targets = [
    'A' => [13109, '18105', '/tmp/zhct-http-ai-macos.zjaU8h'],
    'B' => [13127, '18101', '/tmp/zhct-http-ai-macos.x2CCZX'],
    'C' => [53772, '18102', '/tmp/zhct-ai-http-20260929.3XDj3w'],
];
[$pid, $port, $root] = $targets[$lane];
$loadedRoot = $lane === 'C'
    ? '/tmp/zhct-dm-poc-20260928.fcpiiN/ai_api'
    : ($lane === 'A'
        ? '/Users/wiscourper/Documents/workspace/local/dm8-full-acceptance.fF5yTy/baseline-a-ai-api-V4.0.40'
        : '/Users/wiscourper/Documents/workspace/local/dm8-full-acceptance.fF5yTy/ai_api');
require_once $root . '/vendor/autoload.php';
foreach ([
    'app\\api\\service\\user\\AiConversation' =>
        ['app/api/service/user/AiConversation.php', $sourceSha],
    'app\\api\\controller\\user\\AiConversation' =>
        ['app/api/controller/user/AiConversation.php',
            '9ab9c645e754edb8afe7ac80fef8fadc3b5cf6232af2239b7e49d1cf1db8c3ab'],
] as $class => [$relative, $expectedSha]) {
    $loaded = (new ReflectionClass($class))->getFileName();
    if (realpath((string)$loaded) !== realpath($loadedRoot . '/' . $relative)
        || hash_file('sha256', (string)$loaded) !== $expectedSha) {
        throw new RuntimeException('Composer-loaded business source mismatch');
    }
}
if ($lane === 'C') {
    $cmd = file_get_contents('/proc/' . $pid . '/cmdline');
    $rawEnv = file_get_contents('/proc/' . $pid . '/environ');
    if ($cmd === false || $rawEnv === false
        || strpos($cmd, '127.0.0.1:' . $port) === false
        || strpos($cmd, $root . '/public') === false) {
        throw new RuntimeException('C isolated process changed');
    }
    if (hash_file('sha256', $root . '/config/database.php') !==
        'e7402b1ad50fcf4205326d93dc22e4f491c5052c60fcbe36214cd4e07b55343c') {
        throw new RuntimeException('C runtime DB config source drift');
    }
    $env = [];
    foreach (explode("\0", $rawEnv) as $part) {
        if (strpos($part, 'POC_') !== 0) continue;
        $at = strpos($part, '=');
        if ($at !== false) $env[substr($part, 0, $at)] = substr($part, $at + 1);
    }
    foreach (['POC_DIALECT' => 'dm', 'POC_EXTERNAL_STUBS' => '1',
        'POC_AI_HOST' => '127.0.0.1', 'POC_AI_PORT' => '5238',
        'POC_AI_SCHEMA' => 'AIZHCT_APP'] as $key => $expected) {
        if (($env[$key] ?? null) !== $expected) throw new RuntimeException('C AI DB boundary mismatch');
    }
    if (empty($env['POC_AI_USER']) || empty($env['POC_AI_PASSWORD'])) {
        throw new RuntimeException('C private AI DB config unavailable');
    }
    $dbParams = ['dm:host=127.0.0.1;port=5238;schema=AIZHCT_APP',
        $env['POC_AI_USER'], $env['POC_AI_PASSWORD']];
    $expectedFingerprint = '628c12663da929e593de74c9c58e5bcfab0530fbfbcb1d90a9ed7b736a45fd5e';
} else {
    $ps = (string)shell_exec('/bin/ps eww -p ' . $pid . ' -o command=');
    if (strpos($ps, '127.0.0.1:' . $port) === false
        || strpos($ps, $root . '/public') === false) {
        throw new RuntimeException('A/B isolated process changed');
    }
    $listener = (string)shell_exec('/usr/sbin/lsof -nP -a -p ' . $pid
        . ' -iTCP:' . $port . ' -sTCP:LISTEN');
    if (strpos($listener, 'TCP 127.0.0.1:' . $port . ' (LISTEN)') === false) {
        throw new RuntimeException('A/B PID does not own listener');
    }
    $expectedConfigSha = $lane === 'A'
        ? 'e7402b1ad50fcf4205326d93dc22e4f491c5052c60fcbe36214cd4e07b55343c'
        : '8c46a6d9cd277f2950818d09162f639a62f135a5eb6553e6c61ce2c19167fd02';
    if (hash_file('sha256', $root . '/config/database.php') !== $expectedConfigSha) {
        throw new RuntimeException('A/B runtime DB config source drift');
    }
    $path = realpath((string)getenv('POC_MYSQL_LOCAL_ENV'));
    if ($path !== '/Users/wiscourper/Documents/workspace/zhct/zhctproject/ai_api/.env.local') {
        throw new RuntimeException('Wrong local development config');
    }
    $config = parse_ini_file($path, true, INI_SCANNER_RAW);
    $main = $config['DATABASE'] ?? [];
    foreach (['HOSTNAME', 'HOSTPORT', 'DATABASE', 'USERNAME', 'PASSWORD'] as $key) {
        if (empty($main[$key])) throw new RuntimeException('Missing local dev DB setting');
    }
    $namedStore = $config['DATABASE_ZHCT'] ?? [];
    $hasProcessEnv = static function (string $name, string $value) use ($ps): bool {
        return $value !== '' && !preg_match('/\s/', $value)
            && preg_match('/(?:^|\s)' . preg_quote($name . '=' . $value, '/')
                . '(?=\s|$)/', $ps) === 1;
    };
    foreach (['DATABASE' => 'POC_AI_', 'DATABASE_ZHCT' => 'POC_'] as $section => $prefix) {
        $values = $section === 'DATABASE' ? $main : $namedStore;
        foreach (['HOSTNAME' => 'HOST', 'HOSTPORT' => 'PORT',
            'DATABASE' => 'SCHEMA', 'USERNAME' => 'USER',
            'PASSWORD' => 'PASSWORD'] as $from => $to) {
            if (!isset($values[$from]) || !$hasProcessEnv($prefix . $to, (string)$values[$from])) {
                throw new RuntimeException('A/B runtime DB config differs from code local');
            }
        }
    }
    $dbParams = ['mysql:host=' . $main['HOSTNAME'] . ';port=' . $main['HOSTPORT']
        . ';dbname=' . $main['DATABASE'] . ';charset=' . ($main['CHARSET'] ?? 'utf8'),
        $main['USERNAME'], $main['PASSWORD']];
    $expectedFingerprint = '210f925e093f71e26a8c8923f0f626e3be0a15ccff3d386bba21feef695c8a5b';
}
$base = 'http://127.0.0.1:' . $port;
$boundary = json_decode((string)file_get_contents($base . '/__poc_boundary'), true);
if (!is_array($boundary) || ($boundary['external_stubs'] ?? false) !== true) {
    throw new RuntimeException('External stub boundary missing');
}
$openDb = static function () use ($dbParams, $lane, $expectedFingerprint): PDO {
    $options = [
        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
        PDO::ATTR_CASE => PDO::CASE_LOWER,
    ];
    if ($lane !== 'C') $options[PDO::ATTR_EMULATE_PREPARES] = false;
    $pdo = new PDO($dbParams[0], $dbParams[1], $dbParams[2], $options);
    if ($lane === 'C') {
        $owner = strtoupper((string)$pdo->query('SELECT USER FROM DUAL')->fetchColumn());
        $schema = strtoupper((string)$pdo->query(
            "SELECT SYS_CONTEXT('USERENV','CURRENT_SCHEMA') FROM DUAL")->fetchColumn());
        $identity = $owner . '|5238|' . $schema;
    } else {
        $identity = (string)$pdo->query(
            "SELECT CONCAT(@@server_uuid,'|',@@hostname,'|',@@port,'|',DATABASE())"
        )->fetchColumn();
    }
    if (hash('sha256', $identity) !== $expectedFingerprint) {
        throw new RuntimeException('Approved AI database fingerprint mismatch');
    }
    return $pdo;
};
$pdo = $openDb();
if ($lane === 'C') {
    $machineId = trim((string)@file_get_contents('/etc/machine-id'));
    $host = gethostname();
    if ($machineId === '' || !is_string($host)
        || hash('sha256', $host . '|' . $machineId)
            !== 'd56d81988bb0f106946cce91c608fdd0ef1116e5150b23376ea87e086eace4b9') {
        throw new RuntimeException('C approved host identity mismatch');
    }
    $listeners = [];
    @exec('/usr/sbin/ss -ltnp 2>/dev/null', $listeners);
    $dmListener = false;
    foreach ($listeners as $line) {
        if (preg_match('/\*:5238\s+.*users:\(\("dmserver",pid=(\d+),/', $line, $match)) {
            $exe = @readlink('/proc/' . $match[1] . '/exe');
            $dmListener = is_string($exe) && basename($exe) === 'dmserver';
            break;
        }
    }
    if (!$dmListener) throw new RuntimeException('C DM5238 listener is not local dmserver');
    foreach (['YOSHOP_AI_CONVERSATION_SESSION', 'YOSHOP_AI_CONVERSATION'] as $table) {
        $ddl = $pdo->query("SELECT DBMS_METADATA.GET_DDL('TABLE','" . $table
            . "') FROM DUAL")->fetchColumn();
        $ddlText = is_resource($ddl) ? stream_get_contents($ddl) : (string)$ddl;
        if (!preg_match('/"ID"\s+(?:BIGINT|INT)\s+AUTO_INCREMENT\b/i', (string)$ddlText)) {
            throw new RuntimeException('C fixture table ID is not auto increment');
        }
    }
    $indexes = $pdo->query("SELECT i.INDEX_NAME,c.COLUMN_NAME,c.COLUMN_POSITION "
        . "FROM USER_INDEXES i JOIN USER_IND_COLUMNS c ON c.INDEX_NAME=i.INDEX_NAME "
        . "WHERE i.TABLE_NAME='YOSHOP_AI_CONVERSATION_SESSION' "
        . "AND i.UNIQUENESS='UNIQUE' ORDER BY i.INDEX_NAME,c.COLUMN_POSITION")
        ->fetchAll(PDO::FETCH_ASSOC);
    $groups = [];
    foreach ($indexes as $index) $groups[$index['index_name']][] = strtoupper((string)$index['column_name']);
    if (!in_array(['STORE_ID', 'USER_ID', 'AI_USER_ID', 'BOT_ID', 'CONVERSATION_ID'],
        array_values($groups), true)) {
        throw new RuntimeException('C exact owner unique key missing');
    }
} else {
    foreach (['yoshop_ai_conversation_session', 'yoshop_ai_conversation'] as $table) {
        $meta = $pdo->prepare('SELECT EXTRA FROM information_schema.COLUMNS '
            . "WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME=? AND COLUMN_NAME='id'");
        $meta->execute([$table]);
        if (stripos((string)$meta->fetchColumn(), 'auto_increment') === false) {
            throw new RuntimeException('A/B fixture ID is not auto increment');
        }
    }
    $idx = $pdo->query("SELECT INDEX_NAME,COLUMN_NAME FROM information_schema.STATISTICS "
        . "WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='yoshop_ai_conversation_session' "
        . "AND NON_UNIQUE=0 ORDER BY INDEX_NAME,SEQ_IN_INDEX")->fetchAll(PDO::FETCH_ASSOC);
    $groups = [];
    foreach ($idx as $index) $groups[$index['index_name']][] = strtoupper((string)$index['column_name']);
    if (!in_array(['STORE_ID', 'USER_ID', 'AI_USER_ID', 'BOT_ID', 'CONVERSATION_ID'],
        array_values($groups), true)) {
        throw new RuntimeException('A/B exact owner unique key missing');
    }
}
$whereSession = 'store_id=10001 AND user_id=0 AND ai_user_id=? AND bot_id=? AND conversation_id=?';
$whereMessage = 'store_id=10001 AND user_id=? AND bot_id=? AND conversation_id=?';
$sessionCount = $pdo->prepare('SELECT COUNT(*) FROM yoshop_ai_conversation_session WHERE ' . $whereSession);
$messageCount = $pdo->prepare('SELECT COUNT(*) FROM yoshop_ai_conversation WHERE ' . $whereMessage);
$countOwned = static function () use ($sessionCount, $messageCount, $marker, $botId, $conversationId): array {
    $sessionCount->execute([$marker, $botId, $conversationId]);
    $messageCount->execute([$marker, $botId, $conversationId]);
    return [(int)$sessionCount->fetchColumn(), (int)$messageCount->fetchColumn()];
};
if ($countOwned() !== [0, 0]) throw new RuntimeException('Synthetic marker already exists');
if ($preflight) {
    echo json_encode(['status' => 'read-only-preflight-pass', 'lane' => $lane,
        'marker' => $marker, 'owned_session_and_message_rows' => [0, 0],
        'database_fingerprint' => $expectedFingerprint,
        'loaded_controller_and_service_sha_verified' => true,
        'listener_config_schema_unique_identity_verified' => true,
        'http_and_dml' => 0], JSON_UNESCAPED_SLASHES), PHP_EOL;
    exit(0);
}
// The installed client generates a per-device guest_<timestamp><random>; this
// unique guest_cdx marker is never a shared anonymous identity.
$journalDir = '/tmp/zhct-ai-chat-fixtures-' . $lane;
if (!is_dir($journalDir) && !mkdir($journalDir, 0700)) {
    throw new RuntimeException('Private journal directory unavailable');
}
if (is_link($journalDir) || !is_dir($journalDir)
    || fileowner($journalDir) !== posix_geteuid()
    || (fileperms($journalDir) & 0777) !== 0700) {
    throw new RuntimeException('Private journal directory ownership or mode mismatch');
}
$journal = $journalDir . '/' . $suffix . '.json';
if (file_exists($journal)) throw new RuntimeException('Private journal already exists');
$record = ['lane' => $lane, 'marker' => $marker,
    'bot_id' => $botId, 'conversation_id' => $conversationId,
    'database_fingerprint' => $expectedFingerprint, 'phase' => 'before-insert'];
$oldUmask = umask(0077);
$written = file_put_contents($journal, json_encode($record), LOCK_EX);
umask($oldUmask);
if ($written === false) {
    throw new RuntimeException('Private journal unavailable');
}
chmod($journal, 0600);
if (is_link($journal) || fileowner($journal) !== posix_geteuid()
    || (fileperms($journal) & 0777) !== 0600) {
    throw new RuntimeException('Private journal ownership or mode mismatch');
}
$sessionId = 0;
$messageId = 0;
$httpResults = [];
$failure = null;
$cleaned = false;
try {
    $pdo->beginTransaction();
    $now = time();
    $insertSession = $pdo->prepare('INSERT INTO yoshop_ai_conversation_session '
        . '(store_id,user_id,ai_user_id,open_id,bot_id,conversation_id,title,last_message,'
        . 'message_count,is_current,is_delete,create_time,update_time) '
        . 'VALUES (10001,0,?,\'\',?,?,?,?,1,1,0,?,?)');
    $insertSession->execute([$marker, $botId, $conversationId, $contentText, $contentText, $now, $now]);
    $insertMessage = $pdo->prepare('INSERT INTO yoshop_ai_conversation '
        . '(user_id,store_id,open_id,bot_id,conversation_id,message_key,'
        . 'content_file_url,upload_file_url,content,deal_flag,is_delete) '
        . 'VALUES (?,10001,\'\',?,?,?,\'\',\'\',?,0,0)');
    $insertMessage->execute([$marker, $botId, $conversationId,
        hash('sha256', $marker . $conversationId), $content]);
    if ($countOwned() !== [1, 1]) throw new RuntimeException('Fixture not exactly one row per table');
    $idSession = $pdo->prepare('SELECT id FROM yoshop_ai_conversation_session WHERE ' . $whereSession);
    $idMessage = $pdo->prepare('SELECT id FROM yoshop_ai_conversation WHERE ' . $whereMessage);
    $idSession->execute([$marker, $botId, $conversationId]);
    $idMessage->execute([$marker, $botId, $conversationId]);
    $sessionId = (int)$idSession->fetchColumn();
    $messageId = (int)$idMessage->fetchColumn();
    if ($sessionId < 1 || $messageId < 1) throw new RuntimeException('Fixture identity unavailable');
    $pdo->commit();
    $record['session_id'] = $sessionId;
    $record['message_id'] = $messageId;
    $record['phase'] = 'committed';
    file_put_contents($journal, json_encode($record), LOCK_EX);

    $cases = [
        ['sessionList', ['ai_user_id' => $marker, 'bot_id' => $botId, 'page' => 1, 'pagesize' => 10]],
        ['currentSession', ['ai_user_id' => $marker, 'bot_id' => $botId]],
        ['lists', ['ai_user_id' => $marker, 'bot_id' => $botId,
            'conversation_id' => $conversationId, 'page' => 1, 'pagesize' => 10]],
    ];
    foreach ($cases as [$action, $params]) {
        $url = $base . '/api/user.AiConversation/' . $action . '?storeId=10001';
        $ctx = stream_context_create(['http' => ['method' => 'POST', 'ignore_errors' => true,
            'timeout' => 15, 'header' => "platform: POC\r\nContent-Type: application/json\r\n",
            'content' => json_encode($params, JSON_UNESCAPED_SLASHES)]]);
        $start = microtime(true);
        $stream = @fopen($url, 'rb', false, $ctx);
        if ($stream === false) throw new RuntimeException('Bounded HTTP stream unavailable');
        try {
            $raw = stream_get_contents($stream, 1048577);
        } finally {
            fclose($stream);
        }
        if (!is_string($raw) || strlen($raw) > 1048576) {
            throw new RuntimeException('HTTP response exceeded one-MiB limit');
        }
        $elapsed = (int)round((microtime(true) - $start) * 1000);
        $line = $http_response_header[0] ?? '';
        $http = preg_match('/HTTP\/\S+\s+(\d+)/', $line, $match) ? (int)$match[1] : 0;
        $body = json_decode($raw, true);
        $data = is_array($body) ? ($body['data'] ?? null) : null;
        $selected = null;
        if ($action === 'sessionList') {
            $rows = $data['sessions']['rows'] ?? null;
            if (is_array($rows) && count($rows) === 1) $selected = $rows[0];
            $positive = is_array($selected) && (int)($selected['id'] ?? 0) === $sessionId
                && (int)($data['sessions']['total'] ?? -1) === 1
                && hash_equals($contentText, (string)($selected['first_message'] ?? ''));
        } elseif ($action === 'currentSession') {
            $selected = $data['session'] ?? null;
            $positive = is_array($selected) && (int)($selected['id'] ?? 0) === $sessionId
                && (string)($selected['conversation_id'] ?? '') === $conversationId;
        } else {
            $rows = $data['lists']['rows'] ?? null;
            if (is_array($rows) && count($rows) === 1) $selected = $rows[0];
            $positive = is_array($selected) && (int)($selected['id'] ?? 0) === $messageId
                && (int)($data['lists']['total'] ?? -1) === 1
                && is_array($selected['content'] ?? null)
                && ($selected['content'][0]['content'] ?? null) === $contentText;
        }
        $httpResults[$action] = ['http' => $http,
            'business_status' => is_array($body) ? ($body['status'] ?? null) : null,
            'elapsed_ms' => $elapsed, 'positive_own_row' => $positive,
            'selected_id_type' => is_array($selected) ? gettype($selected['id'] ?? null) : null,
            'selected_keys' => is_array($selected) ? array_keys($selected) : null,
            'response_sha256' => is_string($raw) ? hash('sha256', $raw) : null];
        if ($http !== 200 || !is_array($body) || (int)($body['status'] ?? 0) !== 200
            || !$positive) throw new RuntimeException('HTTP/source contract mismatch');
    }
    if ($countOwned() !== [1, 1]) throw new RuntimeException('Read APIs changed fixture rows');
} catch (Throwable $error) {
    $failure = ['class' => get_class($error), 'code' => (string)$error->getCode()];
} finally {
    if ($pdo->inTransaction()) $pdo->rollBack();
    if ($sessionId > 0 && $messageId > 0) {
        try {
            $pdo->beginTransaction();
            $deleteMessage = $pdo->prepare('DELETE FROM yoshop_ai_conversation WHERE id=? AND ' . $whereMessage);
            $deleteSession = $pdo->prepare('DELETE FROM yoshop_ai_conversation_session WHERE id=? AND ' . $whereSession);
            $deleteMessage->execute([$messageId, $marker, $botId, $conversationId]);
            $deleteSession->execute([$sessionId, $marker, $botId, $conversationId]);
            if ($deleteMessage->rowCount() !== 1 || $deleteSession->rowCount() !== 1) {
                throw new RuntimeException('Exact owned-row cleanup count mismatch');
            }
            $pdo->commit();
            $cleaned = $countOwned() === [0, 0];
            $record['phase'] = $cleaned ? 'cleaned' : 'cleanup-unverified';
        } catch (Throwable $cleanupError) {
            if ($pdo->inTransaction()) $pdo->rollBack();
            $record['phase'] = 'cleanup-failed';
            $failure = ['class' => get_class($cleanupError),
                'code' => (string)$cleanupError->getCode(), 'stage' => 'cleanup'];
        }
    } elseif ($countOwned() === [0, 0]) {
        $cleaned = true;
        $record['phase'] = 'cleaned-no-committed-fixture';
    } else {
        $record['phase'] = 'cleanup-required';
    }
    file_put_contents($journal, json_encode($record), LOCK_EX);
}
$pass = $failure === null && $cleaned && count($httpResults) === 3;
echo json_encode(['status' => $pass ? 'chat-guest-pass' : 'chat-guest-fail',
    'lane' => $lane, 'marker' => $marker, 'fixture_ids' => [$sessionId, $messageId],
    'http' => $httpResults, 'exact_cleanup_zero_same_process' => $cleaned,
    'journal' => $journal, 'failure' => $failure,
    'database_fingerprint' => $expectedFingerprint,
    'source_sha256' => $sourceSha], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), PHP_EOL;
exit($pass ? 0 : 3);
