<?php
declare(strict_types=1);

// Synthetic e-menu device log 20/21 HTTP batch. Dormant until fresh review.
$lane = (string)getenv('CDX_EM_LOG_LANE');
if (!in_array($lane, ['A', 'B', 'C'], true)
    || count($argv) !== 2 || ($argv[1] ?? '') !== '--execute'
    || (string)getenv('CDX_EM_LOG_RUN') !== 'ROOT_APPROVED_OWNED_20_21_LOGS_V1'
    || !hash_equals((string)hash_file('sha256', __FILE__),
        (string)getenv('CDX_EM_LOG_RUN_SHA'))) {
    echo json_encode(['status' => 'default-deny', 'db_connected' => false,
        'business_http_sent' => false, 'dml_sent' => false]), PHP_EOL;
    exit(78);
}
// Existing 1-log evidence does not prove insertAll atomicity or 4-byte charset.
// Reviewed for synthetic-only execution; exact approval string and self-SHA
// are still mandatory and are granted separately for each lane.
const LOG_BATCH_EXECUTION_REVIEWED = true;
if (!LOG_BATCH_EXECUTION_REVIEWED) {
    echo json_encode(['status' => 'review-blocked', 'db_connected' => false,
        'business_http_sent' => false, 'dml_sent' => false]), PHP_EOL;
    exit(78);
}
$stage = 'select_only_preflight';
$httpSent = false;
set_exception_handler(static function (Throwable $error) use (&$stage, &$httpSent): void {
    echo json_encode(['status' => 'redacted_stop', 'stage' => $stage,
        'error_class' => get_class($error), 'code' => (string)$error->getCode(),
        'business_http_sent' => $httpSent,
        'recovery' => 'retain_private_journal_no_blind_retry']), 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);
});
$preflight = __DIR__ . '/preflight_log_batch.php';
if (!hash_equals('8c619b96c447f849b551b5c7119007fee90a34d9ce623eb05e9027280a87fef9',
    (string)hash_file('sha256', $preflight))) {
    throw new RuntimeException('Preflight script drift');
}
putenv('CDX_EM_LOG_PREFLIGHT=OWNED_DEVICE_LOG_BATCH_SELECT_ONLY_V1');
putenv('CDX_EM_LOG_PREFLIGHT_SHA=8c619b96c447f849b551b5c7119007fee90a34d9ce623eb05e9027280a87fef9');
$outerArgv = $argv;
$argv[1] = '--preflight-only';
ob_start();
require $preflight;
$preflightText = (string)ob_get_clean();
$argv = $outerArgv;
restore_exception_handler();
$preflightResult = json_decode(trim($preflightText), true);
if (!is_array($preflightResult)
    || ($preflightResult['status'] ?? null) !== 'select_only_pass'
    || ($preflightResult['lane'] ?? null) !== $lane
    || ($preflightResult['prior_owned_marker_counts'] ?? null)
        !== ['menu' => 0, 'device' => 0, 'log' => 0]
    || ($preflightResult['auto_id_on_three_tables'] ?? null) !== true
    || (int)($preflightResult['trigger_count'] ?? -1) !== 0
    || (int)($preflightResult['incoming_outgoing_fk_count'] ?? -1) !== 0) {
    throw new RuntimeException('Current source/owned-zero preflight failed');
}
$ownsListener = static function (int $targetPid, int $targetPort): bool {
    $hex = strtoupper(str_pad(dechex($targetPort), 4, '0', STR_PAD_LEFT));
    $lines = @file('/proc/net/tcp', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
    if (!is_array($lines)) return false;
    $inodes = [];
    foreach (array_slice($lines, 1) as $line) {
        $parts = preg_split('/\s+/', trim($line));
        if (is_array($parts) && ($parts[1] ?? '') === '0100007F:' . $hex
            && ($parts[3] ?? '') === '0A') $inodes[] = (string)($parts[9] ?? '');
    }
    if (count($inodes) !== 1 || $inodes[0] === '') return false;
    $fds = glob('/proc/' . $targetPid . '/fd/*');
    if (!is_array($fds)) return false;
    foreach ($fds as $fd) {
        if (@readlink($fd) === 'socket:[' . $inodes[0] . ']') return true;
    }
    return false;
};
if (!$ownsListener($pid, $port)) {
    throw new RuntimeException('Fixed PID does not own loopback listener');
}
$stage = 'exclusive_owned_fixture';
$lock = fopen(sys_get_temp_dir() . '/cdx-em-log-'
    . ($dialect === 'dm' ? 'c' : 'dev') . '.lock', 'c');
if ($lock === false || !flock($lock, LOCK_EX | LOCK_NB)) {
    throw new RuntimeException('A/B or C exclusive writer lock unavailable');
}
$table = static function (string $short) use ($dialect): string {
    if (!in_array($short, ['electronic_menu', 'electronic_menu_device',
        'electronic_menu_device_log'], true)) {
        throw new RuntimeException('Fixture table not allowlisted');
    }
    return $dialect === 'dm' ? strtoupper('ydy_' . $short) : 'ydy_' . $short;
};
$connect = static function () use ($dsn, $dbUser, $dbPassword): PDO {
    return new PDO($dsn, $dbUser, $dbPassword,
        [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_CASE => PDO::CASE_LOWER, PDO::ATTR_TIMEOUT => 5]);
};
$marker = 'CDXEMLOG' . strtoupper(bin2hex(random_bytes(6)));
$menuUuid = strtoupper(bin2hex(random_bytes(16)));
$deviceUuid = strtoupper(bin2hex(random_bytes(16)));
$deviceToken = bin2hex(random_bytes(32));
$publicToken = bin2hex(random_bytes(32));
$sourceBefore = [];
foreach (['electronic_menu', 'electronic_menu_device',
    'electronic_menu_device_log'] as $short) {
    $sourceBefore[$short] = hash('sha256', serialize($db->query('SELECT * FROM '
        . $table($short) . ' ORDER BY ID')->fetchAll(PDO::FETCH_ASSOC)));
}
$journalDir = sys_get_temp_dir() . '/cdx-em-log-' . strtolower($marker);
if (file_exists($journalDir) || !mkdir($journalDir, 0700)) {
    throw new RuntimeException('Private recovery journal collision');
}
chmod($journalDir, 0700);
$journalPath = $journalDir . '/recovery.json';
$journal = ['lane' => $lane, 'marker' => $marker,
    'menu_uuid' => $menuUuid, 'device_uuid' => $deviceUuid,
    'public_token' => $publicToken, 'menu_id' => 0,
    'device_record_id' => 0, 'source_before' => $sourceBefore,
    'phase' => 'prepared'];
$saveJournal = static function (array $change) use (&$journal, $journalPath): void {
    $journal = array_merge($journal, $change);
    $text = json_encode($journal, JSON_UNESCAPED_SLASHES);
    if (!is_string($text) || file_put_contents($journalPath, $text, LOCK_EX) === false
        || !chmod($journalPath, 0600)) {
        throw new RuntimeException('Private recovery journal write failed');
    }
};
$saveJournal([]);
$stage = 'synthetic_menu_and_device_insert';
date_default_timezone_set('PRC');
$now = date('Y-m-d H:i:s');
$restaurant = $db->query('SELECT LEFT_RESTAURANT_ID FROM '
    . $table('electronic_menu')
    . ' WHERE STORE_ID=10001 AND STATUS=1 AND IS_DELETE=0 ORDER BY ID')
    ->fetch(PDO::FETCH_ASSOC);
$restaurantId = (int)($restaurant['left_restaurant_id'] ?? 0);
if ($restaurantId <= 0) throw new RuntimeException('No read-only restaurant relation');
$db->prepare('INSERT INTO ' . $table('electronic_menu')
    . ' (UUID,PUBLIC_TOKEN,STORE_ID,STATUS,IS_DELETE,DISPLAY_MODE,'
    . ' LEFT_RESTAURANT_ID,RIGHT_RESTAURANT_ID,CAROUSEL_INTERVAL,CREATE_BY,'
    . ' CREATE_TIME,UPDATE_TIME) VALUES (?,?,10001,1,0,1,?,0,8,?,?,?)')
    ->execute([$menuUuid, $publicToken, $restaurantId, $marker, $now, $now]);
$stmt = $connect()->prepare('SELECT ID FROM ' . $table('electronic_menu')
    . ' WHERE UUID=? AND PUBLIC_TOKEN=? AND CREATE_BY=?');
$stmt->execute([$menuUuid, $publicToken, $marker]);
$menuIds = $stmt->fetchAll(PDO::FETCH_COLUMN);
if (count($menuIds) !== 1 || (int)$menuIds[0] <= 0) {
    throw new RuntimeException('Owned menu insert ambiguous');
}
$menuId = (int)$menuIds[0];
$saveJournal(['phase' => 'menu_inserted', 'menu_id' => $menuId]);
$sentinel = '2000-01-01 00:00:00';
$db->prepare('INSERT INTO ' . $table('electronic_menu_device')
    . ' (UUID,DEVICE_ID,DEVICE_TOKEN_HASH,DEVICE_NAME,MENU_ID,STATUS,'
    . ' LAST_SEEN_TIME,UPDATE_TIME,STORE_ID,IS_DELETE,CREATE_BY,CREATE_TIME)'
    . ' VALUES (?,?,?,?,?,1,?,?,10001,0,?,?)')
    ->execute([$deviceUuid, $marker, hash('sha256', $deviceToken),
        $marker, $menuId, $sentinel, $now, $marker, $now]);
$stmt = $connect()->prepare('SELECT ID FROM ' . $table('electronic_menu_device')
    . ' WHERE UUID=? AND DEVICE_ID=? AND MENU_ID=? AND CREATE_BY=?');
$stmt->execute([$deviceUuid, $marker, $menuId, $marker]);
$deviceIds = $stmt->fetchAll(PDO::FETCH_COLUMN);
if (count($deviceIds) !== 1 || (int)$deviceIds[0] <= 0) {
    throw new RuntimeException('Owned device insert ambiguous');
}
$deviceId = (int)$deviceIds[0];
$saveJournal(['phase' => 'device_inserted', 'device_record_id' => $deviceId]);
$logTable = $table('electronic_menu_device_log');
$count = $connect()->prepare('SELECT COUNT(*) FROM ' . $logTable
    . ' WHERE DEVICE_RECORD_ID=? AND MENU_ID=?');
$count->execute([$deviceId, $menuId]);
if ((int)$count->fetchColumn() !== 0) {
    throw new RuntimeException('Owned log precondition not zero');
}
$httpSent = true;
$post = static function (array $logs) use ($port, $marker, $deviceToken): array {
    $payload = json_encode($logs, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
    if (!is_string($payload) || strlen($payload) > 10000) {
        throw new RuntimeException('Bounded log request payload invalid');
    }
    $form = http_build_query(['device_id' => $marker,
        'device_token' => $deviceToken, 'logs' => $payload], '', '&', PHP_QUERY_RFC3986);
    $context = stream_context_create(['http' => [
        'method' => 'POST', 'timeout' => 12, 'ignore_errors' => true,
        'follow_location' => 0,
        'header' => "Content-Type: application/x-www-form-urlencoded\r\n",
        'content' => $form,
    ]]);
    $stream = @fopen('http://127.0.0.1:' . $port
        . '/api/electronicMenu/device/logs', 'rb', false, $context);
    $body = $stream === false ? false : stream_get_contents($stream, 1048577);
    $headers = $http_response_header ?? [];
    if (is_resource($stream)) fclose($stream);
    preg_match('/HTTP\/\S+\s+(\d+)/', (string)($headers[0] ?? ''), $match);
    if (!is_string($body) || strlen($body) > 1048576) {
        throw new RuntimeException('Bounded log HTTP response invalid');
    }
    $json = json_decode($body, true);
    return ['http' => (int)($match[1] ?? 0),
        'json' => is_array($json) ? $json : null];
};
$logs = [];
$validOccurred = [];
for ($i = 1; $i <= 20; $i++) {
    $occurred = date('Y-m-d H:i:s', time() - 60 * $i);
    $validOccurred[$i] = $occurred;
    $logs[] = [
        'level' => ['info', 'warning', 'error'][($i - 1) % 3],
        'event_code' => sprintf('CDXEMLOG.N%02d', $i),
        'error_code' => sprintf('CDXEMLOG.E%02d', $i),
        'message' => sprintf('批量日志测试%02d', $i),
        'context' => ['android_version' => '13',
            'manufacturer' => '测试厂商', 'http_status' => 200,
            'unsafe_key' => 'must_be_removed'],
        'occurred_at' => $i === 20 ? 'not-a-date' : $occurred,
    ];
}
$results = [];
$failure = '';
try {
    $stage = 'twenty_log_http';
    $sentAt = time();
    $saveJournal(['phase' => 'http20_started']);
    $twenty = $post($logs);
    $afterAt = time();
    if ($twenty['http'] !== 200 || ($twenty['json']['code'] ?? null) !== 0
        || (int)($twenty['json']['data']['accepted'] ?? -1) !== 20) {
        throw new RuntimeException('20-log HTTP response contract failed');
    }
    $read = $connect();
    $stmt = $read->prepare('SELECT * FROM ' . $logTable
        . ' WHERE DEVICE_RECORD_ID=? AND MENU_ID=? ORDER BY ID');
    $stmt->execute([$deviceId, $menuId]);
    $stored = $stmt->fetchAll(PDO::FETCH_ASSOC);
    if (count($stored) !== 20) throw new RuntimeException('insertAll did not write 20 rows');
    $uuids = [];
    foreach ($stored as $row) {
        $event = (string)$row['event_code'];
        if (!preg_match('/^CDXEMLOG\.N(0[1-9]|1[0-9]|20)$/D', $event)) {
            throw new RuntimeException('Batch event identity mismatch');
        }
        $n = (int)substr($event, -2);
        $expected = $logs[$n - 1];
        $context = json_decode((string)$row['context_json'], true);
        if ((int)$row['id'] <= 0 || (string)$row['uuid'] === ''
            || (int)$row['store_id'] !== 10001
            || (string)$row['create_by'] !== 'device:' . $marker
            || (string)$row['level'] !== $expected['level']
            || (string)$row['error_code'] !== $expected['error_code']
            || (string)$row['message'] !== $expected['message']
            || !is_array($context)
            || ($context['android_version'] ?? null) !== '13'
            || ($context['manufacturer'] ?? null) !== '测试厂商'
            || (string)($context['http_status'] ?? '') !== '200'
            || isset($context['unsafe_key'])) {
            throw new RuntimeException('Batch row/JSON contract mismatch');
        }
        $when = strtotime((string)$row['occurred_at']);
        if ($n === 20) {
            if ($when === false || $when < $sentAt - 2 || $when > $afterAt + 2) {
                throw new RuntimeException('Invalid time fallback mismatch');
            }
        } elseif ($when === false
            || $when !== strtotime($validOccurred[$n])) {
            throw new RuntimeException('Valid occurred_at timestamp mismatch');
        }
        $uuids[] = (string)$row['uuid'];
    }
    if (count(array_unique($uuids)) !== 20) {
        throw new RuntimeException('Batch UUID collision');
    }
    $deviceStmt = $read->prepare('SELECT LAST_SEEN_TIME FROM '
        . $table('electronic_menu_device') . ' WHERE ID=?');
    $deviceStmt->execute([$deviceId]);
    $lastSeen = strtotime((string)$deviceStmt->fetchColumn());
    if ($lastSeen === false || $lastSeen < $sentAt - 2 || $lastSeen > $afterAt + 2) {
        throw new RuntimeException('Device last_seen_time not advanced');
    }
    $results['twenty_inserted_and_readback'] = true;
    $stage = 'twenty_one_rejected_zero_write';
    $before21 = hash('sha256', serialize($stored));
    $beforeDevice = $read->prepare('SELECT * FROM '
        . $table('electronic_menu_device') . ' WHERE ID=?');
    $beforeDevice->execute([$deviceId]);
    $deviceHash = hash('sha256', serialize($beforeDevice->fetch(PDO::FETCH_ASSOC)));
    $saveJournal(['phase' => 'http21_started']);
    $twentyOne = $post(array_merge($logs, [$logs[0]]));
    $stmt->execute([$deviceId, $menuId]);
    $afterLogs = $stmt->fetchAll(PDO::FETCH_ASSOC);
    $beforeDevice->execute([$deviceId]);
    $afterDeviceHash = hash('sha256', serialize($beforeDevice->fetch(PDO::FETCH_ASSOC)));
    if ($twentyOne['http'] !== 200 || ($twentyOne['json']['code'] ?? null) !== 1
        || ($twentyOne['json']['data'] ?? null) !== []
        || hash('sha256', serialize($afterLogs)) !== $before21
        || $afterDeviceHash !== $deviceHash) {
        throw new RuntimeException('21-log rejection or zero-write mismatch');
    }
    $results['twenty_one_rejected_no_mutation'] = true;
} catch (Throwable $error) {
    $failure = get_class($error) . ':' . hash('sha256', $error->getMessage());
}
$stage = 'fixed_worker_completion_barrier';
$socket = @stream_socket_client('tcp://127.0.0.1:' . $port, $errno, $errstr, 5);
$socketConnected = is_resource($socket);
if ($socketConnected) fclose($socket);
$cmdAfter = (string)@file_get_contents('/proc/' . $pid . '/cmdline');
$childrenAfter = trim((string)@file_get_contents('/proc/' . $pid
    . '/task/' . $pid . '/children'));
$boundaryAfter = json_decode((string)@file_get_contents(
    'http://127.0.0.1:' . $port . '/__poc_boundary', false,
    stream_context_create(['http' => ['timeout' => 8]])), true);
if (!$socketConnected || strpos($cmdAfter, $root . '/public') === false
    || strpos($cmdAfter, '127.0.0.1:' . $port) === false
    || $childrenAfter !== '' || !$ownsListener($pid, $port)
    || !is_array($boundaryAfter)
    || ($boundaryAfter['external_stubs'] ?? null) !== true
    || ($boundaryAfter['redis_isolated'] ?? null) !== true
    || ($boundaryAfter['real_queue'] ?? true) !== false) {
    throw new RuntimeException('HTTP completion uncertain; do not cleanup');
}
$saveJournal(['phase' => 'http_finished_barrier']);
$stage = 'exact_owned_transaction_cleanup';
$cleanup = $connect();
$logsToDelete = $cleanup->prepare('SELECT ID,UUID,CREATE_BY FROM ' . $logTable
    . ' WHERE DEVICE_RECORD_ID=? AND MENU_ID=? ORDER BY ID');
$logsToDelete->execute([$deviceId, $menuId]);
$ownedLogs = $logsToDelete->fetchAll(PDO::FETCH_ASSOC);
if (count($ownedLogs) > 20) throw new RuntimeException('Unexpected owned log cleanup scope');
foreach ($ownedLogs as $row) {
    if ((string)$row['create_by'] !== 'device:' . $marker
        || (int)$row['id'] <= 0 || (string)$row['uuid'] === '') {
        throw new RuntimeException('Unrecognized owned log row');
    }
}
$cleanup->beginTransaction();
foreach ($ownedLogs as $row) {
    $delete = $cleanup->prepare('DELETE FROM ' . $logTable
        . ' WHERE ID=? AND UUID=? AND DEVICE_RECORD_ID=?'
        . ' AND MENU_ID=? AND CREATE_BY=?');
    $delete->execute([(int)$row['id'], (string)$row['uuid'],
        $deviceId, $menuId, 'device:' . $marker]);
    if ($delete->rowCount() !== 1) throw new RuntimeException('Owned log delete count not one');
}
$deleteDevice = $cleanup->prepare('DELETE FROM '
    . $table('electronic_menu_device')
    . ' WHERE ID=? AND UUID=? AND DEVICE_ID=? AND MENU_ID=? AND CREATE_BY=?');
$deleteDevice->execute([$deviceId, $deviceUuid, $marker, $menuId, $marker]);
if ($deleteDevice->rowCount() !== 1) throw new RuntimeException('Owned device delete not one');
$deleteMenu = $cleanup->prepare('DELETE FROM ' . $table('electronic_menu')
    . ' WHERE ID=? AND UUID=? AND PUBLIC_TOKEN=? AND CREATE_BY=?');
$deleteMenu->execute([$menuId, $menuUuid, $publicToken, $marker]);
if ($deleteMenu->rowCount() !== 1) throw new RuntimeException('Owned menu delete not one');
if (!$cleanup->commit()) throw new RuntimeException('Owned cleanup commit uncertain');
$saveJournal(['phase' => 'cleanup_committed_pending_independent_audit']);
echo json_encode(['status' => $failure === ''
        ? 'scoped_pass_pending_independent_audit'
        : 'scoped_fail_pending_independent_audit',
    'lane' => $lane, 'results' => $results, 'failure' => $failure,
    'owned_log_rows_deleted' => count($ownedLogs),
    'journal' => $journalPath,
    'business_http_sent' => true], JSON_UNESCAPED_SLASHES), PHP_EOL;
if ($failure !== '') exit(1);
