<?php

declare(strict_types=1);

// A/B-only short probe: one public activation against a synthetic expired code.
$lane = (string)getenv('CDX_EXPIRED_SHORT_LANE');
$action = (string)($argv[1] ?? '');
$targets = [
    'A' => ['/tmp/zhct-http-store-a.l5omH8', 'http://127.0.0.1:18097', 21023,
        '525caaeb63bc83a4f401de077b0784de49b98572b3b7732577b143138d9e7e28'],
    'B' => ['/tmp/zhct-http-store-mysql.dQTTBP', 'http://127.0.0.1:18081', 10001,
        '179869c96cfdaf5e9de1003ace06949dc654bc4876efe89ec53455a7721e47cd'],
];
if (!isset($targets[$lane]) || !in_array($action, ['--preflight', '--execute'], true)
    || ($action === '--execute'
        && ((string)getenv('CDX_EXPIRED_SHORT_EXECUTE') !== 'ROOT_APPROVED_V1'
            || (string)getenv('CDX_EXPIRED_SHORT_SHA') !== hash_file('sha256', __FILE__)))) {
    fwrite(STDERR, "Expired-code short probe disabled\n");
    exit(78);
}
[$root, $base, $pid, $serviceHash] = $targets[$lane];
if ((string)getenv('POC_APP_ROOT') !== $root
    || (string)getenv('POC_BASE_URL') !== $base
    || (string)getenv('POC_DIALECT') !== 'mysql'
    || hash_file('sha256', $root . '/application/common/service/ElectronicMenu.php')
        !== $serviceHash
    || hash_file('sha256', $root . '/application/common/service/ElectronicMenuDevice.php')
        !== '3335769aee94126f7abfae2f2a305ba09cca9270df2ab7590657c00fabef855f') {
    throw new RuntimeException('Approved isolated MySQL source/runtime mismatch');
}
$singleWorker = static function (int $listenerPid): bool {
    $env = (string)@file_get_contents('/proc/' . $listenerPid . '/environ');
    $children = (string)@file_get_contents('/proc/' . $listenerPid
        . '/task/' . $listenerPid . '/children');
    if ($env === '' || trim($children) !== '') return false;
    if (preg_match('/(?:^|\x00)PHP_CLI_SERVER_WORKERS=(\d+)(?:\x00|$)/',
        $env, $match) && (int)$match[1] > 1) return false;
    return true;
};
$runtimeReady = static function () use ($pid, $root, $base, $singleWorker): bool {
    $cmdline = (string)@file_get_contents('/proc/' . $pid . '/cmdline');
    $boundary = json_decode((string)@file_get_contents($base . '/__poc_boundary'), true);
    $portHex = strtoupper(str_pad(dechex((int)parse_url($base, PHP_URL_PORT)),
        4, '0', STR_PAD_LEFT));
    $socketInode = '';
    foreach (@file('/proc/net/tcp', FILE_IGNORE_NEW_LINES) ?: [] as $line) {
        $fields = preg_split('/\s+/', trim($line));
        if (($fields[1] ?? '') === '0100007F:' . $portHex
            && ($fields[3] ?? '') === '0A') {
            $socketInode = (string)($fields[9] ?? '');
            break;
        }
    }
    $portOwnedByPid = false;
    if ($socketInode !== '') {
        foreach (@scandir('/proc/' . $pid . '/fd') ?: [] as $fd) {
            if ($fd === '.' || $fd === '..') continue;
            if (@readlink('/proc/' . $pid . '/fd/' . $fd)
                === 'socket:[' . $socketInode . ']') {
                $portOwnedByPid = true;
                break;
            }
        }
    }
    return strpos($cmdline, '127.0.0.1:' . parse_url($base, PHP_URL_PORT)) !== false
        && strpos($cmdline, $root . '/public') !== false
        && $singleWorker($pid) && $portOwnedByPid
        && is_array($boundary) && !empty($boundary['external_stubs'])
        && !empty($boundary['redis_isolated']) && empty($boundary['real_queue']);
};
if (!$runtimeReady()) throw new RuntimeException('Isolated HTTP boundary absent');
$local = (static function (): array {
    $config = require '/workspace/zhct/zhctproject/store/application/config/local/database.php';
    if (!is_array($config)) throw new RuntimeException('Invalid code-local DB config');
    return $config;
})();
$runtime = (static function (string $path): array {
    $config = require $path;
    if (!is_array($config)) throw new RuntimeException('Invalid runtime DB config');
    return $config;
})($root . '/application/database.php');
foreach (['hostname', 'database', 'username', 'password'] as $key) {
    if (($runtime[$key] ?? null) !== ($local[$key] ?? null)) {
        throw new RuntimeException('Runtime differs from code-local MySQL');
    }
}
if ((string)($runtime['type'] ?? '') !== 'mysql'
    || (string)($runtime['prefix'] ?? '') !== 'ydy_'
    || !in_array((string)($runtime['charset'] ?? ''), ['utf8', 'utf8mb4'], true)
    || (int)($runtime['hostport'] ?? 3306) !== (int)($local['hostport'] ?? 3306)) {
    throw new RuntimeException('MySQL type/prefix/port mismatch');
}
$dsn = 'mysql:host=' . $runtime['hostname'] . ';port='
    . (int)($runtime['hostport'] ?? 3306) . ';dbname=' . $runtime['database']
    . ';charset=' . (string)($runtime['charset'] ?? 'utf8');
$connect = static function () use ($dsn, $runtime): PDO {
    return new PDO($dsn, $runtime['username'], $runtime['password'],
        [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_CASE => PDO::CASE_LOWER, PDO::ATTR_TIMEOUT => 5]);
};
$db = $connect();
$identity = (string)$db->query("SELECT CONCAT(@@server_uuid,'|',@@hostname,'|',@@port,'|',DATABASE())")
    ->fetchColumn();
$dbFingerprint = hash('sha256', $identity);
if ($action === '--execute'
    && (string)getenv('CDX_EXPIRED_SHORT_DB_SHA') !== $dbFingerprint) {
    throw new RuntimeException('Approved MySQL instance fingerprint mismatch');
}
$candidate = $db->query('SELECT LEFT_RESTAURANT_ID FROM ydy_electronic_menu'
    . ' WHERE STORE_ID=10001 AND STATUS=1 AND IS_DELETE=0 ORDER BY ID')
    ->fetch(PDO::FETCH_ASSOC);
$restaurantId = (int)($candidate['left_restaurant_id'] ?? 0);
if ($restaurantId <= 0) throw new RuntimeException('No safe source restaurant');
$uniqueCodeIndex = (int)$db->query('SELECT COUNT(*) FROM information_schema.STATISTICS'
    . " WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='ydy_electronic_menu_bind_code'"
    . " AND INDEX_NAME='uk_code_hash' AND COLUMN_NAME='code_hash' AND NON_UNIQUE=0")
    ->fetchColumn();
if ($uniqueCodeIndex !== 1) {
    throw new RuntimeException('Binding code hash unique index not proven');
}
$tables = ['ydy_electronic_menu', 'ydy_electronic_menu_bind_code',
    'ydy_electronic_menu_device', 'ydy_electronic_menu_device_log'];
$before = [];
foreach ($tables as $table) {
    $rows = $db->query('SELECT * FROM ' . $table . ' ORDER BY ID')
        ->fetchAll(PDO::FETCH_ASSOC);
    if (count($rows) > 1000) throw new RuntimeException('Source table exceeds review limit');
    $before[$table] = ['count' => count($rows),
        'sha256' => hash('sha256', serialize($rows))];
}
$stale = (int)$db->query("SELECT COUNT(*) FROM ydy_electronic_menu"
    . " WHERE CREATE_BY LIKE 'CDXEM%'")->fetchColumn();
$staleRelated = (int)$db->query("SELECT COUNT(*) FROM ydy_electronic_menu_bind_code"
    . " WHERE CREATE_BY LIKE 'CDXEM%'")->fetchColumn();
$staleRelated += (int)$db->query("SELECT COUNT(*) FROM ydy_electronic_menu_device"
    . " WHERE DEVICE_ID LIKE 'CDXEM%'")->fetchColumn();
if ($action === '--preflight') {
    echo json_encode(['lane' => $lane, 'mode' => 'select-only-preflight',
        'database_fingerprint' => $dbFingerprint,
        'existing_synthetic_menus' => $stale,
        'existing_synthetic_related_rows' => $staleRelated,
        'unique_code_hash_index' => true,
        'table_counts' => array_map(static function ($row) { return $row['count']; }, $before),
        'isolated' => true], JSON_UNESCAPED_SLASHES) . PHP_EOL;
    exit(0);
}
if ($stale !== 0 || $staleRelated !== 0) {
    throw new RuntimeException('Old synthetic menu/device data requires review');
}
$marker = 'CDXEM' . strtoupper(bin2hex(random_bytes(8)));
$journalDir = sys_get_temp_dir() . '/zhct-expired-short-' . $marker;
if (file_exists($journalDir) || !mkdir($journalDir, 0700)) {
    throw new RuntimeException('Private recovery directory unavailable');
}
chmod($journalDir, 0700);
$journal = ['lane' => $lane, 'marker' => $marker,
    'database_fingerprint' => $dbFingerprint, 'status' => 'prepared',
    'menu_id' => 0, 'bind_id' => 0];
$saveJournal = static function (array $changes) use (&$journal, $journalDir): void {
    $journal = array_merge($journal, $changes);
    $next = $journalDir . '/recovery.next';
    if (file_put_contents($next, json_encode($journal, JSON_UNESCAPED_SLASHES)) === false) {
        throw new RuntimeException('Recovery journal write failed');
    }
    chmod($next, 0600);
    if (!rename($next, $journalDir . '/recovery.json')) {
        throw new RuntimeException('Recovery journal replace failed');
    }
};
$saveJournal([]);
$menuId = 0;
$bindId = 0;
$token = bin2hex(random_bytes(32));
$code = '';
for ($attempt = 0; $attempt < 20; $attempt++) {
    $candidateCode = (string)random_int(100000, 999999);
    $candidateHash = hash('sha256', $candidateCode);
    $query = $db->prepare('SELECT COUNT(*) FROM ydy_electronic_menu_bind_code'
        . ' WHERE CODE_HASH=?');
    $query->execute([$candidateHash]);
    if ((int)$query->fetchColumn() === 0) {
        $code = $candidateCode;
        break;
    }
}
if ($code === '') throw new RuntimeException('No unused synthetic binding code');
$codeHash = hash('sha256', $code);
$results = [];
$cleanup = [];
$failure = '';
try {
    $now = date('Y-m-d H:i:s');
    $stmt = $db->prepare('INSERT INTO ydy_electronic_menu'
        . ' (UUID,PUBLIC_TOKEN,DISPLAY_MODE,LEFT_RESTAURANT_ID,'
        . ' RIGHT_RESTAURANT_ID,CAROUSEL_INTERVAL,STATUS,STORE_ID,IS_DELETE,'
        . ' CREATE_BY,CREATE_TIME,UPDATE_TIME) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)');
    $stmt->execute([$marker, $token, 1, $restaurantId, 0, 8,
        1, 10001, 0, $marker, $now, $now]);
    $stmt = $db->prepare('SELECT ID FROM ydy_electronic_menu'
        . ' WHERE UUID=? AND PUBLIC_TOKEN=? AND CREATE_BY=?');
    $stmt->execute([$marker, $token, $marker]);
    $menuId = (int)$stmt->fetchColumn();
    if ($menuId <= 0) throw new RuntimeException('Short menu fixture missing');
    $saveJournal(['status' => 'menu_inserted', 'menu_id' => $menuId]);

    $expiredAt = date('Y-m-d H:i:s', time() - 60);
    $stmt = $db->prepare('INSERT INTO ydy_electronic_menu_bind_code'
        . ' (MENU_ID,CODE_HASH,EXPIRES_AT,USED_AT,STATUS,STORE_ID,IS_DELETE,'
        . ' CREATE_BY,CREATE_TIME,UPDATE_TIME) VALUES (?,?,?,?,?,?,?,?,?,?)');
    $stmt->execute([$menuId, $codeHash, $expiredAt, null, 1,
        10001, 0, $marker, $now, $now]);
    $stmt = $db->prepare('SELECT ID FROM ydy_electronic_menu_bind_code'
        . ' WHERE MENU_ID=? AND CODE_HASH=? AND CREATE_BY=?');
    $stmt->execute([$menuId, $codeHash, $marker]);
    $bindId = (int)$stmt->fetchColumn();
    if ($bindId <= 0) throw new RuntimeException('Short binding fixture missing');
    $saveJournal(['status' => 'bind_inserted', 'bind_id' => $bindId]);

    $bindBefore = $db->query('SELECT * FROM ydy_electronic_menu_bind_code'
        . ' WHERE ID=' . $bindId)->fetch(PDO::FETCH_ASSOC);
    $query = $db->prepare('SELECT COUNT(*) FROM ydy_electronic_menu_bind_code'
        . ' WHERE CODE_HASH=?');
    $query->execute([$codeHash]);
    if (!is_array($bindBefore)
        || (int)($bindBefore['id'] ?? 0) !== $bindId
        || (int)($bindBefore['menu_id'] ?? 0) !== $menuId
        || (string)($bindBefore['code_hash'] ?? '') !== $codeHash
        || substr((string)($bindBefore['expires_at'] ?? ''), 0, 19) !== $expiredAt
        || (int)($bindBefore['status'] ?? 0) !== 1
        || ($bindBefore['used_at'] ?? null) !== null
        || (string)($bindBefore['create_by'] ?? '') !== $marker
        || (int)($bindBefore['store_id'] ?? 0) !== 10001
        || (int)$query->fetchColumn() !== 1) {
        throw new RuntimeException('Synthetic expired binding identity mismatch');
    }
    $postBody = http_build_query(['bind_code' => $code,
        'device_id' => $marker . 'E', 'device_name' => $marker], '', '&', PHP_QUERY_RFC3986);
    $ctx = stream_context_create(['http' => ['method' => 'POST',
        'header' => "Content-Type: application/x-www-form-urlencoded\r\n",
        'content' => $postBody, 'timeout' => 20, 'ignore_errors' => true]]);
    $body = @file_get_contents($base . '/api/electronicMenu/device/activate', false, $ctx);
    $headers = $http_response_header ?? [];
    preg_match('/HTTP\/\S+\s+(\d+)/', (string)($headers[0] ?? ''), $httpMatch);
    $response = is_string($body) ? json_decode($body, true) : null;
    $bindAfter = $db->query('SELECT * FROM ydy_electronic_menu_bind_code'
        . ' WHERE ID=' . $bindId)->fetch(PDO::FETCH_ASSOC);
    $stmt = $db->prepare('SELECT COUNT(*) FROM ydy_electronic_menu_device'
        . ' WHERE DEVICE_ID=?');
    $stmt->execute([$marker . 'E']);
    if ((int)($httpMatch[1] ?? 0) !== 200 || !is_array($response)
        || ($response['code'] ?? null) !== 1
        || ($response['message'] ?? null) !== '绑定码无效或已过期'
        || ($response['data'] ?? null) !== []
        || serialize($bindBefore) !== serialize($bindAfter)
        || (int)$stmt->fetchColumn() !== 0) {
        throw new RuntimeException('Short expired-code contract/source mismatch');
    }
    $results['expired_code_rejected_zero_write'] = true;
} catch (Throwable $e) {
    $failure = get_class($e) . ':' . hash('sha256', $e->getMessage());
} finally {
    $cleanup['barrier_safe'] = $runtimeReady();
    if ($cleanup['barrier_safe']) {
        try {
            $db = $connect();
            $menu = $db->prepare('SELECT ID,UUID,PUBLIC_TOKEN FROM ydy_electronic_menu'
                . ' WHERE CREATE_BY=?');
            $menu->execute([$marker]);
            $menus = $menu->fetchAll(PDO::FETCH_ASSOC);
            if (count($menus) > 1) throw new RuntimeException('Ambiguous short menu cleanup');
            $binding = $db->prepare('SELECT ID,MENU_ID,CODE_HASH,CREATE_BY'
                . ' FROM ydy_electronic_menu_bind_code WHERE CREATE_BY=?');
            $binding->execute([$marker]);
            $bindings = $binding->fetchAll(PDO::FETCH_ASSOC);
            if (count($bindings) > 1) throw new RuntimeException('Ambiguous short bind cleanup');
            $devices = $db->prepare('SELECT ID,UUID,DEVICE_ID,MENU_ID,CREATE_BY'
                . ' FROM ydy_electronic_menu_device WHERE DEVICE_ID=?');
            $devices->execute([$marker . 'E']);
            $deviceRows = $devices->fetchAll(PDO::FETCH_ASSOC);
            if (count($deviceRows) > 1) throw new RuntimeException('Ambiguous short device cleanup');
            foreach ($deviceRows as $row) {
                if ((int)$row['menu_id'] !== (int)($menus[0]['id'] ?? 0)
                    || (string)$row['create_by'] !== 'device-bind') {
                    throw new RuntimeException('Unrecognized short device row');
                }
            }
            $db->beginTransaction();
            foreach ($deviceRows as $row) {
                $stmt = $db->prepare('DELETE FROM ydy_electronic_menu_device'
                    . ' WHERE ID=? AND UUID=? AND DEVICE_ID=? AND MENU_ID=?');
                $stmt->execute([(int)$row['id'], (string)$row['uuid'],
                    (string)$row['device_id'], (int)$row['menu_id']]);
            }
            foreach ($bindings as $row) {
                if ((int)$row['menu_id'] !== (int)($menus[0]['id'] ?? 0)
                    || (string)$row['code_hash'] !== $codeHash) {
                    throw new RuntimeException('Unrecognized short binding row');
                }
                $stmt = $db->prepare('DELETE FROM ydy_electronic_menu_bind_code'
                    . ' WHERE ID=? AND MENU_ID=? AND CODE_HASH=? AND CREATE_BY=?');
                $stmt->execute([(int)$row['id'], (int)$row['menu_id'],
                    $codeHash, $marker]);
            }
            foreach ($menus as $row) {
                if (($menuId > 0 && (int)$row['id'] !== $menuId)
                    || (string)$row['uuid'] !== $marker
                    || (string)$row['public_token'] !== $token) {
                    throw new RuntimeException('Unrecognized short menu row');
                }
                $stmt = $db->prepare('DELETE FROM ydy_electronic_menu'
                    . ' WHERE ID=? AND UUID=? AND PUBLIC_TOKEN=? AND CREATE_BY=?');
                $stmt->execute([(int)$row['id'], $marker, $token, $marker]);
            }
            $db->commit();
        } catch (Throwable $e) {
            if (isset($db) && $db->inTransaction()) {
                try { $db->rollBack(); } catch (Throwable $ignored) {}
            }
            $cleanup['error_class'] = get_class($e);
            $cleanup['error_sha256'] = hash('sha256', $e->getMessage());
        }
    }
    try {
        $check = $connect();
        $restored = true;
        foreach ($tables as $table) {
            $rows = $check->query('SELECT * FROM ' . $table . ' ORDER BY ID')
                ->fetchAll(PDO::FETCH_ASSOC);
            $restored = $restored && count($rows) === $before[$table]['count']
                && hash('sha256', serialize($rows)) === $before[$table]['sha256'];
        }
        $cleanup['source_restored'] = $restored;
        $stmt = $check->prepare('SELECT COUNT(*) FROM ydy_electronic_menu'
            . ' WHERE CREATE_BY=?');
        $stmt->execute([$marker]);
        $cleanup['marker_zero'] = (int)$stmt->fetchColumn() === 0;
    } catch (Throwable $e) {
        $cleanup['verify_error_class'] = get_class($e);
    }
}
$clean = !empty($cleanup['barrier_safe']) && !empty($cleanup['source_restored'])
    && !empty($cleanup['marker_zero']) && empty($cleanup['error_class']);
try {
    $saveJournal(['status' => $clean ? 'cleaned' : 'needs_recovery',
        'failure' => $failure, 'cleanup' => $cleanup]);
} catch (Throwable $e) {
    $cleanup['journal_error_class'] = get_class($e);
    $clean = false;
}
echo json_encode(['lane' => $lane, 'suite' => 'expired-code-short',
    'marker' => $marker, 'journal_dir' => $journalDir,
    'results' => $results, 'failure' => $failure, 'cleanup' => $cleanup],
    JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL;
if ($failure !== '' || !$clean) exit(1);
