<?php
declare(strict_types=1);

// Default DENIED. One short-lived synthetic guest session/message, one write
// action plus negative/repeat HTTP, exact cleanup. Root review before execution.
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_WRITE_EXECUTE') !== 'ROOT_REVIEWED_CHAT_WRITE_V1') {
    echo json_encode(['status' => 'DENIED', 'reason' => 'root-review-required']), PHP_EOL;
    exit(0);
}
$lane = (string)getenv('CDX_CHAT_LANE');
$action = (string)getenv('CDX_CHAT_ACTION');
$marker = (string)getenv('CDX_CHAT_MARKER');
$selfSha = (string)getenv('CDX_CHAT_WRITE_SCRIPT_SHA');
if (!in_array($lane, ['A', 'B', 'C'], true)
    || !in_array($action, ['deleteSession', 'clearSession'], true)
    || !preg_match('/^guest_cdx[a-f0-9]{16}$/', $marker)
    || $selfSha === '' || hash_file('sha256', __FILE__) !== $selfSha) {
    throw new RuntimeException('Exact lane/action/marker/script gate failed');
}
$suffix = substr($marker, strlen('guest_cdx'));
$botId = 'cdxchatbot';
$conversationId = 'cdxchat_' . $suffix;
$contentText = '本次会话删除兼容测试文本';
$content = json_encode([['role' => 'user', 'content' => $contentText]], JSON_UNESCAPED_UNICODE);
$targets = [
    'A' => [13109, '18105', '/tmp/zhct-http-ai-macos.zjaU8h',
        '/Users/wiscourper/Documents/workspace/local/dm8-full-acceptance.fF5yTy/baseline-a-ai-api-V4.0.40',
        'e7402b1ad50fcf4205326d93dc22e4f491c5052c60fcbe36214cd4e07b55343c'],
    'B' => [13127, '18101', '/tmp/zhct-http-ai-macos.x2CCZX',
        '/Users/wiscourper/Documents/workspace/local/dm8-full-acceptance.fF5yTy/ai_api',
        '8c46a6d9cd277f2950818d09162f639a62f135a5eb6553e6c61ce2c19167fd02'],
    'C' => [53772, '18102', '/tmp/zhct-ai-http-20260929.3XDj3w',
        '/tmp/zhct-dm-poc-20260928.fcpiiN/ai_api',
        'e7402b1ad50fcf4205326d93dc22e4f491c5052c60fcbe36214cd4e07b55343c'],
];
[$pid, $port, $root, $loadedRoot, $configSha] = $targets[$lane];
if (hash_file('sha256', $root . '/config/database.php') !== $configSha) {
    throw new RuntimeException('Runtime DB config source drift');
}
require_once $root . '/vendor/autoload.php';
foreach ([
    'app\\api\\service\\user\\AiConversation' =>
        ['app/api/service/user/AiConversation.php',
            '4431e6451ac088ad325a23626ee669996dc8fde1e92e721b1eccad085bd3e2fc'],
    'app\\api\\controller\\user\\AiConversation' =>
        ['app/api/controller/user/AiConversation.php',
            '9ab9c645e754edb8afe7ac80fef8fadc3b5cf6232af2239b7e49d1cf1db8c3ab'],
] as $class => [$relative, $expected]) {
    $actual = (new ReflectionClass($class))->getFileName();
    if (realpath((string)$actual) !== realpath($loadedRoot . '/' . $relative)
        || hash_file('sha256', (string)$actual) !== $expected) {
        throw new RuntimeException('Composer-loaded business code drift');
    }
}
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');
    }
    $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 target drift');
    }
    if (empty($env['POC_AI_USER']) || empty($env['POC_AI_PASSWORD'])) {
        throw new RuntimeException('C private AI DB config unavailable');
    }
    $dbArgs = ['dm:host=127.0.0.1;port=5238;schema=AIZHCT_APP',
        $env['POC_AI_USER'], $env['POC_AI_PASSWORD']];
    $fingerprint = '628c12663da929e593de74c9c58e5bcfab0530fbfbcb1d90a9ed7b736a45fd5e';
} else {
    $ps = (string)shell_exec('/bin/ps eww -p ' . $pid . ' -o command=');
    $listener = (string)shell_exec('/usr/sbin/lsof -nP -a -p ' . $pid
        . ' -iTCP:' . $port . ' -sTCP:LISTEN');
    if (strpos($ps, '127.0.0.1:' . $port) === false
        || strpos($ps, $root . '/public') === false
        || strpos($listener, 'TCP 127.0.0.1:' . $port . ' (LISTEN)') === false) {
        throw new RuntimeException('A/B isolated listener ownership 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 code local DB config');
    }
    $config = parse_ini_file($path, true, INI_SCANNER_RAW);
    $main = $config['DATABASE'] ?? [];
    $store = $config['DATABASE_ZHCT'] ?? [];
    $hasEnv = 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 ([['values' => $main, 'prefix' => 'POC_AI_'],
        ['values' => $store, 'prefix' => 'POC_']] as $group) {
        foreach (['HOSTNAME' => 'HOST', 'HOSTPORT' => 'PORT',
            'DATABASE' => 'SCHEMA', 'USERNAME' => 'USER',
            'PASSWORD' => 'PASSWORD'] as $from => $to) {
            if (!isset($group['values'][$from])
                || !$hasEnv($group['prefix'] . $to, (string)$group['values'][$from])) {
                throw new RuntimeException('A/B process DB differs from .env.local');
            }
        }
    }
    $dbArgs = ['mysql:host=' . $main['HOSTNAME'] . ';port=' . $main['HOSTPORT']
        . ';dbname=' . $main['DATABASE'] . ';charset=' . ($main['CHARSET'] ?? 'utf8'),
        $main['USERNAME'], $main['PASSWORD']];
    $fingerprint = '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 unavailable');
}
$openDb = static function () use ($dbArgs, $lane, $fingerprint): PDO {
    $options = [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
        PDO::ATTR_CASE => PDO::CASE_LOWER];
    if ($lane !== 'C') $options[PDO::ATTR_EMULATE_PREPARES] = false;
    $db = new PDO($dbArgs[0], $dbArgs[1], $dbArgs[2], $options);
    if ($lane === 'C') {
        $owner = strtoupper((string)$db->query('SELECT USER FROM DUAL')->fetchColumn());
        $schema = strtoupper((string)$db->query(
            "SELECT SYS_CONTEXT('USERENV','CURRENT_SCHEMA') FROM DUAL")->fetchColumn());
        $identity = $owner . '|5238|' . $schema;
    } else {
        $identity = (string)$db->query(
            "SELECT CONCAT(@@server_uuid,'|',@@hostname,'|',@@port,'|',DATABASE())"
        )->fetchColumn();
    }
    if (hash('sha256', $identity) !== $fingerprint) {
        throw new RuntimeException('Approved AI DB fingerprint mismatch');
    }
    return $db;
};
$db = $openDb();
if ($lane === 'C') {
    $host = gethostname();
    $machine = trim((string)@file_get_contents('/etc/machine-id'));
    if (!is_string($host) || $machine === ''
        || hash('sha256', $host . '|' . $machine)
            !== 'd56d81988bb0f106946cce91c608fdd0ef1116e5150b23376ea87e086eace4b9') {
        throw new RuntimeException('C host identity mismatch');
    }
    $ss = [];
    @exec('/usr/sbin/ss -ltnp 2>/dev/null', $ss);
    $owns = false;
    foreach ($ss as $line) {
        if (preg_match('/\*:5238\s+.*users:\(\("dmserver",pid=(\d+),/', $line, $m)) {
            $exe = @readlink('/proc/' . $m[1] . '/exe');
            $owns = is_string($exe) && basename($exe) === 'dmserver';
            break;
        }
    }
    if (!$owns) throw new RuntimeException('C 5238 not local dmserver');
    foreach (['YOSHOP_AI_CONVERSATION_SESSION', 'YOSHOP_AI_CONVERSATION'] as $table) {
        $ddl = $db->query("SELECT DBMS_METADATA.GET_DDL('TABLE','" . $table
            . "') FROM DUAL")->fetchColumn();
        $text = is_resource($ddl) ? stream_get_contents($ddl) : (string)$ddl;
        if (!preg_match('/"ID"\s+(?:BIGINT|INT)\s+AUTO_INCREMENT\b/i', (string)$text)) {
            throw new RuntimeException('C fixture ID not auto increment');
        }
    }
    $idx = $db->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);
    $uniqueGroups = [];
    foreach ($idx as $index) $uniqueGroups[$index['index_name']][] = strtoupper((string)$index['column_name']);
    if (!in_array(['STORE_ID', 'USER_ID', 'AI_USER_ID', 'BOT_ID', 'CONVERSATION_ID'],
        array_values($uniqueGroups), true)) {
        throw new RuntimeException('C owner unique index missing');
    }
} else {
    foreach (['yoshop_ai_conversation_session', 'yoshop_ai_conversation'] as $table) {
        $meta = $db->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 not auto increment');
        }
    }
    $idx = $db->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);
    $uniqueGroups = [];
    foreach ($idx as $index) $uniqueGroups[$index['index_name']][] = strtoupper((string)$index['column_name']);
    if (!in_array(['STORE_ID', 'USER_ID', 'AI_USER_ID', 'BOT_ID', 'CONVERSATION_ID'],
        array_values($uniqueGroups), true)) {
        throw new RuntimeException('A/B owner unique index missing');
    }
}
$sessionWhere = 'store_id=10001 AND user_id=0 AND ai_user_id=? AND bot_id=? AND conversation_id=?';
$messageWhere = 'store_id=10001 AND user_id=? AND bot_id=? AND conversation_id=?';
$countSession = $db->prepare('SELECT COUNT(*) FROM yoshop_ai_conversation_session WHERE ' . $sessionWhere);
$countMessage = $db->prepare('SELECT COUNT(*) FROM yoshop_ai_conversation WHERE ' . $messageWhere);
$counts = static function () use ($countSession, $countMessage, $marker, $botId, $conversationId): array {
    $countSession->execute([$marker, $botId, $conversationId]);
    $countMessage->execute([$marker, $botId, $conversationId]);
    return [(int)$countSession->fetchColumn(), (int)$countMessage->fetchColumn()];
};
if ($counts() !== [0, 0]) throw new RuntimeException('Marker already has rows');
if ($preflight) {
    echo json_encode(['status' => 'read-only-preflight-pass', 'lane' => $lane,
        'action' => $action, 'marker' => $marker, 'owned_rows' => [0, 0],
        'database_fingerprint' => $fingerprint, 'http_and_dml' => 0],
        JSON_UNESCAPED_SLASHES), PHP_EOL;
    exit(0);
}
$journalDir = '/tmp/zhct-ai-chat-write-fixtures-' . $lane . '-' . $action;
if (!is_dir($journalDir) && !mkdir($journalDir, 0700)) {
    throw new RuntimeException('Private journal directory unavailable');
}
if (is_link($journalDir) || fileowner($journalDir) !== posix_geteuid()
    || (fileperms($journalDir) & 0777) !== 0700) {
    throw new RuntimeException('Private journal directory unsafe');
}
$journal = $journalDir . '/' . $suffix . '.json';
if (file_exists($journal)) throw new RuntimeException('Journal exists for marker');
$record = ['lane' => $lane, 'action' => $action, 'marker' => $marker,
    'bot_id' => $botId, 'conversation_id' => $conversationId,
    'database_fingerprint' => $fingerprint, 'phase' => 'before-insert'];
$oldUmask = umask(0077);
$writeOk = file_put_contents($journal, json_encode($record), LOCK_EX);
umask($oldUmask);
if ($writeOk === false) throw new RuntimeException('Private journal create failed');
chmod($journal, 0600);
if (is_link($journal) || fileowner($journal) !== posix_geteuid()
    || (fileperms($journal) & 0777) !== 0600) {
    throw new RuntimeException('Private journal unsafe');
}
$sessionId = 0;
$messageId = 0;
$results = [];
$failure = null;
$cleaned = false;
$fetchRows = static function () use ($db, $sessionWhere, $messageWhere,
    $marker, $botId, $conversationId): array {
    $s = $db->prepare('SELECT id,is_delete,is_current,last_message,message_count,update_time '
        . 'FROM yoshop_ai_conversation_session WHERE ' . $sessionWhere);
    $m = $db->prepare('SELECT id,is_delete,content,content_file_url '
        . 'FROM yoshop_ai_conversation WHERE ' . $messageWhere);
    $s->execute([$marker, $botId, $conversationId]);
    $m->execute([$marker, $botId, $conversationId]);
    $session = $s->fetch(PDO::FETCH_ASSOC);
    $message = $m->fetch(PDO::FETCH_ASSOC);
    if (is_array($message)) {
        $storedContent = $message['content'] ?? '';
        $storedContent = is_resource($storedContent)
            ? stream_get_contents($storedContent) : (string)$storedContent;
        $message['content_sha256'] = hash('sha256', (string)$storedContent);
        unset($message['content']);
    }
    return [$session, $message];
};
$request = static function (array $body) use ($base, $action): array {
    $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($body, 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');
    }
    $line = $http_response_header[0] ?? '';
    $http = preg_match('/HTTP\/\S+\s+(\d+)/', $line, $match) ? (int)$match[1] : 0;
    $decoded = json_decode($raw, true);
    return ['http' => $http, 'business_status' => is_array($decoded) ? ($decoded['status'] ?? null) : null,
        'data_type' => is_array($decoded) ? gettype($decoded['data'] ?? null) : null,
        'response_sha256' => hash('sha256', $raw),
        'elapsed_ms' => (int)round((microtime(true) - $start) * 1000)];
};
try {
    $db->beginTransaction();
    $now = time();
    $insertS = $db->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,?,?)');
    $insertS->execute([$marker, $botId, $conversationId, $contentText, $contentText, $now, $now]);
    $insertM = $db->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)');
    $insertM->execute([$marker, $botId, $conversationId,
        hash('sha256', $marker . $conversationId), $content]);
    if ($counts() !== [1, 1]) throw new RuntimeException('Fixture not one row per table');
    [$initialS, $initialM] = $fetchRows();
    $sessionId = (int)($initialS['id'] ?? 0);
    $messageId = (int)($initialM['id'] ?? 0);
    if ($sessionId < 1 || $messageId < 1) throw new RuntimeException('Fixture IDs missing');
    $db->commit();
    $record['session_id'] = $sessionId;
    $record['message_id'] = $messageId;
    $record['phase'] = 'committed';
    file_put_contents($journal, json_encode($record), LOCK_EX);

    $invalid = $request(['ai_user_id' => $marker, 'bot_id' => $botId]);
    [$invalidS, $invalidM] = $fetchRows();
    $negativeZeroWrite = $invalid['http'] === 200
        && (int)$invalid['business_status'] !== 200
        && hash('sha256', json_encode([$initialS, $initialM]))
            === hash('sha256', json_encode([$invalidS, $invalidM]));
    $results['missing_conversation_id'] = $invalid + ['zero_write' => $negativeZeroWrite];
    if (!$negativeZeroWrite) throw new RuntimeException('Missing-ID rejection or zero-write failed');

    $params = ['ai_user_id' => $marker, 'bot_id' => $botId,
        'conversation_id' => $conversationId];
    for ($attempt = 1; $attempt <= 2; ++$attempt) {
        $response = $request($params);
        [$rowS, $rowM] = $fetchRows();
        $ownIds = (int)($rowS['id'] ?? 0) === $sessionId
            && (int)($rowM['id'] ?? 0) === $messageId;
        $messageSoftDeleted = (int)($rowM['is_delete'] ?? -1) === 1;
        $state = $action === 'deleteSession'
            ? (int)($rowS['is_delete'] ?? -1) === 1
                && (int)($rowS['is_current'] ?? -1) === 0
            : (int)($rowS['is_delete'] ?? -1) === 0
                && (string)($rowS['last_message'] ?? '?') === ''
                && (int)($rowS['message_count'] ?? -1) === 0;
        $pass = $response['http'] === 200
            && (int)$response['business_status'] === 200
            && $ownIds && $messageSoftDeleted && $state && $counts() === [1, 1];
        $results['attempt_' . $attempt] = $response + [
            'owned_ids_match' => $ownIds,
            'session_state_match' => $state,
            'message_soft_deleted' => $messageSoftDeleted,
            'readback_pass' => $pass];
        if (!$pass) throw new RuntimeException('Write/repeat original-table contract failed');
    }
} catch (Throwable $error) {
    $failure = ['class' => get_class($error), 'code' => (string)$error->getCode()];
} finally {
    if ($db->inTransaction()) $db->rollBack();
    if ($sessionId > 0 && $messageId > 0) {
        try {
            $db->beginTransaction();
            $dm = $db->prepare('DELETE FROM yoshop_ai_conversation WHERE id=? AND ' . $messageWhere);
            $ds = $db->prepare('DELETE FROM yoshop_ai_conversation_session WHERE id=? AND ' . $sessionWhere);
            $dm->execute([$messageId, $marker, $botId, $conversationId]);
            $ds->execute([$sessionId, $marker, $botId, $conversationId]);
            if ($dm->rowCount() !== 1 || $ds->rowCount() !== 1) {
                throw new RuntimeException('Exact cleanup rowCount mismatch');
            }
            $db->commit();
            $cleaned = $counts() === [0, 0];
            $record['phase'] = $cleaned ? 'cleaned' : 'cleanup-unverified';
        } catch (Throwable $cleanupError) {
            if ($db->inTransaction()) $db->rollBack();
            $record['phase'] = 'cleanup-failed';
            $failure = ['stage' => 'cleanup', 'class' => get_class($cleanupError),
                'code' => (string)$cleanupError->getCode()];
        }
    } elseif ($counts() === [0, 0]) {
        $cleaned = true;
        $record['phase'] = 'cleaned-no-fixture';
    } else {
        $record['phase'] = 'manual-cleanup-required';
    }
    file_put_contents($journal, json_encode($record), LOCK_EX);
}
$pass = $failure === null && $cleaned
    && ($results['missing_conversation_id']['zero_write'] ?? false)
    && ($results['attempt_1']['readback_pass'] ?? false)
    && ($results['attempt_2']['readback_pass'] ?? false);
echo json_encode(['status' => $pass ? 'chat-write-pass' : 'chat-write-fail',
    'lane' => $lane, 'action' => $action, 'marker' => $marker,
    'fixture_ids' => [$sessionId, $messageId], 'results' => $results,
    'exact_cleanup_zero_same_process' => $cleaned,
    'journal' => $journal, 'failure' => $failure,
    'database_fingerprint' => $fingerprint], JSON_UNESCAPED_SLASHES), PHP_EOL;
exit($pass ? 0 : 3);
