<?php

declare(strict_types=1);

// Read-only source and external-integration gate for the three Store screen routes.
$lane = (string)getenv('CDX_KIOSK_LANE');
$method = strtoupper((string)(getenv('CDX_KIOSK_METHOD') ?: 'GET'));
$targets = [
    'A' => ['/tmp/zhct-http-store-a.l5omH8', 'http://127.0.0.1:18097', 21023,
        'mysql', '306f0fd3c2602f3f1c84b6c45b48f9f8456a56669a9028025724d05611962270'],
    'B' => ['/tmp/zhct-http-store-mysql.dQTTBP', 'http://127.0.0.1:18081', 10001,
        'mysql', 'e6071d5bd58547b0ea372a2e638af32f0805001aa55a851b0370e6379fe3721d'],
    'C' => ['/tmp/zhct-dm-poc-20260928.fcpiiN/store', 'http://127.0.0.1:18080', 112175,
        'dm', 'e6071d5bd58547b0ea372a2e638af32f0805001aa55a851b0370e6379fe3721d'],
];
$action = (string)($argv[1] ?? '');
if (!isset($targets[$lane]) || !in_array($method, ['GET', 'POST'], true)
    || !in_array($action, ['--preflight', '--execute-article'], true)
    || ($action === '--execute-article'
        && ((string)getenv('CDX_KIOSK_EXECUTE') !== 'APPROVED_ARTICLE_READONLY_V1'
            || (string)getenv('CDX_KIOSK_SHA') !== hash_file('sha256', __FILE__)))) {
    fwrite(STDERR, "Kiosk execution disabled\n");
    exit(78);
}
[$root, $base, $pid, $dialect, $logicSha] = $targets[$lane];
if ((string)getenv('POC_APP_ROOT') !== $root
    || (string)getenv('POC_BASE_URL') !== $base
    || (string)getenv('POC_DIALECT') !== $dialect
    || is_link($root)
    || hash_file('sha256', $root . '/application/m/controller/ControlScreen.php')
        !== '64726620cbdc77b12e0ab247425ad34d08c05d55cecbc24c1414627b4dcb884f'
    || hash_file('sha256', $root . '/application/m/logic/ControlScreen.php') !== $logicSha) {
    throw new RuntimeException('Approved kiosk source/runtime mismatch');
}
$cmdline = (string)@file_get_contents('/proc/' . $pid . '/cmdline');
$environment = (string)@file_get_contents('/proc/' . $pid . '/environ');
$children = (string)@file_get_contents('/proc/' . $pid . '/task/' . $pid . '/children');
$boundary = json_decode((string)@file_get_contents($base . '/__poc_boundary'), true);
$port = (int)parse_url($base, PHP_URL_PORT);
$hex = strtoupper(str_pad(dechex($port), 4, '0', STR_PAD_LEFT));
$inode = '';
foreach (@file('/proc/net/tcp', FILE_IGNORE_NEW_LINES) ?: [] as $line) {
    $parts = preg_split('/\s+/', trim($line));
    if (($parts[1] ?? '') === '0100007F:' . $hex && ($parts[3] ?? '') === '0A') {
        $inode = (string)($parts[9] ?? '');
        break;
    }
}
$ownsPort = false;
if ($inode !== '') {
    foreach (@scandir('/proc/' . $pid . '/fd') ?: [] as $fd) {
        if ($fd !== '.' && $fd !== '..'
            && @readlink('/proc/' . $pid . '/fd/' . $fd) === 'socket:[' . $inode . ']') {
            $ownsPort = true;
            break;
        }
    }
}
if (strpos($cmdline, '127.0.0.1:' . $port) === false
    || strpos($cmdline, $root . '/public') === false
    || $environment === '' || trim($children) !== '' || !$ownsPort
    || preg_match('/(?:^|\x00)PHP_CLI_SERVER_WORKERS=(\d+)(?:\x00|$)/', $environment, $match)
        && (int)$match[1] > 1
    || !is_array($boundary) || empty($boundary['external_stubs'])
    || empty($boundary['redis_isolated']) || !empty($boundary['real_queue'])) {
    throw new RuntimeException('Kiosk isolated HTTP boundary absent');
}
$integrations = [];
foreach (['JSR_REPORT_ENABLED', 'JSR_REPORT_BASE_URL', 'JSR_READONLY_DB_HOST',
    'JSR_REPORT_ENV_FILE', 'JSR_READONLY_ENV_FILE'] as $key) {
    $integrations[$key] = preg_match('/(?:^|\x00)' . $key . '=[^\x00]*(?:\x00|$)/', $environment) === 1;
}
foreach (['jsr-report.env', 'jsr-db.env'] as $name) {
    $integrations[$name] = is_file($root . '/config/local/' . $name);
}
if (array_filter($integrations)) {
    echo json_encode(['lane' => $lane, 'external_jsr_gate' => 'present',
        'keys' => array_keys(array_filter($integrations)), 'http_allowed' => false],
        JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), PHP_EOL;
    exit(0);
}
$runtime = (static function (string $path): array {
    $value = require $path;
    if (!is_array($value)) throw new RuntimeException('Invalid kiosk DB config');
    return $value;
})($root . '/application/database.php');
if ((string)($runtime['prefix'] ?? '') !== 'ydy_') {
    throw new RuntimeException('Kiosk table prefix mismatch');
}
if ($dialect === 'mysql') {
    $local = (static function (): array {
        $value = require '/workspace/zhct/zhctproject/store/application/config/local/database.php';
        if (!is_array($value)) throw new RuntimeException('Invalid code local DB config');
        return $value;
    })();
    if ((string)($runtime['type'] ?? '') !== 'mysql') {
        throw new RuntimeException('Kiosk MySQL driver mismatch');
    }
    foreach (['hostname', 'database', 'username', 'password'] as $key) {
        if (($runtime[$key] ?? null) !== ($local[$key] ?? null)) {
            throw new RuntimeException('Kiosk runtime differs from code local');
        }
    }
    if ((int)($runtime['hostport'] ?? 3306) !== (int)($local['hostport'] ?? 3306)) {
        throw new RuntimeException('Kiosk MySQL port mismatch');
    }
    $dsn = 'mysql:host=' . $runtime['hostname'] . ';port='
        . (int)($runtime['hostport'] ?? 3306) . ';dbname=' . $runtime['database']
        . ';charset=' . (string)($runtime['charset'] ?? 'utf8');
    $username = (string)$runtime['username'];
    $password = (string)$runtime['password'];
} else {
    if ((string)($runtime['type'] ?? '') !== 'app\\cores\\database\\Dm'
        || (string)getenv('POC_HOST') !== '127.0.0.1'
        || (string)getenv('POC_PORT') !== '5237'
        || strtoupper((string)getenv('POC_SCHEMA')) !== 'ZHCT_APP'
        || strtoupper((string)getenv('POC_USER')) !== 'ZHCT_APP') {
        throw new RuntimeException('Kiosk DM target mismatch');
    }
    foreach (['hostname' => 'POC_HOST', 'hostport' => 'POC_PORT',
        'database' => 'POC_SCHEMA', 'username' => 'POC_USER',
        'password' => 'POC_PASSWORD'] as $key => $envKey) {
        if ((string)($runtime[$key] ?? '') !== (string)getenv($envKey)) {
            throw new RuntimeException('Kiosk DM worker context mismatch');
        }
    }
    $dsn = 'dm:host=127.0.0.1;port=5237;schema=ZHCT_APP';
    $username = (string)getenv('POC_USER');
    $password = (string)getenv('POC_PASSWORD');
    $machineId = trim((string)@file_get_contents('/etc/machine-id'));
    if ($machineId === '' || gethostname() === false
        || hash('sha256', gethostname() . '|' . $machineId)
            !== 'd56d81988bb0f106946cce91c608fdd0ef1116e5150b23376ea87e086eace4b9') {
        throw new RuntimeException('Kiosk DM host mismatch');
    }
    $lines = [];
    $code = 0;
    @exec('/usr/sbin/ss -ltnp 2>/dev/null', $lines, $code);
    $localDm = false;
    if ($code === 0) {
        foreach ($lines as $line) {
            if (preg_match('/\*:5237\s+.*users:\(\("dmserver",pid=(\d+),/', $line, $match)) {
                $exe = @readlink('/proc/' . $match[1] . '/exe');
                $localDm = is_string($exe) && basename($exe) === 'dmserver';
                break;
            }
        }
    }
    if (!$localDm) throw new RuntimeException('Kiosk DM listener not local');
}
$pdo = new PDO($dsn, $username, $password, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_CASE => PDO::CASE_LOWER, PDO::ATTR_TIMEOUT => 5]);
if ($dialect === 'mysql') {
    $identity = (string)$pdo->query(
        "SELECT CONCAT(@@server_uuid,'|',@@hostname,'|',@@port,'|',DATABASE())"
    )->fetchColumn();
    $approved = '003eb5aecf1cd4595b609c1f7f20b9ee325b578309fb9fe14fedaf77584fa401';
} else {
    $schema = (string)$pdo->query(
        "SELECT SYS_CONTEXT('USERENV','CURRENT_SCHEMA') FROM DUAL"
    )->fetchColumn();
    if (strtoupper($schema) !== 'ZHCT_APP') throw new RuntimeException('Kiosk DM schema mismatch');
    $identity = 'ZHCT_APP|5237|' . $schema;
    $approved = 'bc98485aed26beab348770a82ae38dbf922b04ab88a210cd7282c4caf0156194';
}
if (!hash_equals($approved, hash('sha256', $identity))) {
    throw new RuntimeException('Kiosk approved DB identity mismatch');
}
$prefix = $dialect === 'dm' ? 'YDY_' : 'ydy_';
$articleCount = (int)$pdo->query('SELECT COUNT(*) FROM ' . $prefix . 'article WHERE STATUS=1')
    ->fetchColumn();
$articleSample = $pdo->query('SELECT ID,UUID,TITLE,STATUS FROM ' . $prefix
    . 'article WHERE STATUS=1 ORDER BY ID ASC');
$ids = [];
while (($row = $articleSample->fetch(PDO::FETCH_ASSOC)) !== false && count($ids) < 20) {
    $normal = array_change_key_case($row, CASE_LOWER);
    $ids[] = [(int)$normal['id'], (string)$normal['uuid'], (string)$normal['title'],
        (int)$normal['status']];
}
$areas = $pdo->query('SELECT ID,NAME FROM ' . $prefix . 'restaurant_area'
    . " WHERE IS_DELETE=0 AND STATUS=1 AND (NAME LIKE '%南京%' OR NAME LIKE '%镇江%')"
    . ' ORDER BY ID ASC')->fetchAll(PDO::FETCH_ASSOC);
$areaIds = array_map(static function ($row): int {
    return (int)array_change_key_case($row, CASE_LOWER)['id'];
}, $areas);
$result = [
    'lane' => $lane, 'dialect' => $dialect, 'external_jsr_gate' => 'absent',
    'http_allowed' => true, 'published_article_count' => $articleCount,
    'published_article_sample_count' => count($ids),
    'published_article_sample_sha' => hash('sha256', serialize($ids)),
    'nanjing_zhenjiang_active_area_count' => count($areaIds),
    'nanjing_zhenjiang_area_ids' => $areaIds,
];
if ($action === '--preflight') {
    echo json_encode($result, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), PHP_EOL;
    exit(0);
}

if ($articleCount < 1 || $articleCount > 500) {
    throw new RuntimeException('Article positive source absent or exceeds bounded route page');
}
$toText = static function ($value): ?string {
    if ($value === null) return null;
    return is_resource($value) ? (string)stream_get_contents($value) : (string)$value;
};
$fetchArticles = static function () use ($pdo, $prefix, $toText): array {
    $stmt = $pdo->query('SELECT * FROM ' . $prefix
        . 'article WHERE STATUS=1 ORDER BY ID ASC');
    $rows = [];
    while (($row = $stmt->fetch(PDO::FETCH_ASSOC)) !== false) {
        $normal = [];
        foreach (array_change_key_case($row, CASE_LOWER) as $key => $value) {
            $normal[$key] = $toText($value);
        }
        $rows[] = $normal;
        if (count($rows) > 500) throw new RuntimeException('Article source grew beyond 500 rows');
    }
    return $rows;
};
$fetchCategories = static function () use ($pdo, $prefix, $toText): array {
    $stmt = $pdo->query('SELECT * FROM ' . $prefix . 'article_cate ORDER BY ID ASC');
    $rows = [];
    while (($row = $stmt->fetch(PDO::FETCH_ASSOC)) !== false) {
        $normal = [];
        foreach (array_change_key_case($row, CASE_LOWER) as $key => $value) {
            $normal[$key] = $toText($value);
        }
        $rows[] = $normal;
        if (count($rows) > 1000) throw new RuntimeException('Article category source exceeds bound');
    }
    return $rows;
};
$articlesBefore = $fetchArticles();
$categoriesBefore = $fetchCategories();
$sourceSha = hash('sha256', serialize([$articlesBefore, $categoriesBefore]));
if (count($articlesBefore) !== $articleCount) {
    throw new RuntimeException('Article source count drift before HTTP');
}
$url = $base . '/m/controlScreen/articleSnapshot';
$context = stream_context_create(['http' => ['method' => $method,
    'header' => "Accept: application/json\r\n", 'timeout' => 20,
    'ignore_errors' => true]]);
$httpStarted = microtime(true);
$stream = @fopen($url, 'rb', false, $context);
if (!$stream) throw new RuntimeException('Article HTTP open failed');
$headers = $http_response_header ?? [];
$body = stream_get_contents($stream, 8388609);
fclose($stream);
$httpMs = (int)round((microtime(true) - $httpStarted) * 1000);
if (!is_string($body) || strlen($body) > 8388608
    || strpos((string)($headers[0] ?? ''), ' 200 ') === false) {
    throw new RuntimeException('Article HTTP status or 8 MiB response bound mismatch');
}
$decoded = json_decode($body, true);
if (!is_array($decoded)
    || array_keys($decoded) !== ['code', 'message', 'data']
    || ($decoded['code'] ?? null) !== 0
    || ($decoded['message'] ?? null) !== '获取成功'
    || !is_array($decoded['data'] ?? null)
    || array_keys($decoded['data']) !== ['rows', 'total']
    || !is_array($decoded['data']['rows'])
    || !is_int($decoded['data']['total'])
    || $decoded['data']['total'] !== $articleCount
    || count($decoded['data']['rows']) !== $articleCount) {
    throw new RuntimeException('Article HTTP envelope/count contract mismatch');
}
$types = [];
$allKeys = [];
$firstRowTypes = [];
foreach ($articlesBefore as $index => $source) {
    $row = $decoded['data']['rows'][$index] ?? null;
    if (!is_array($row)) throw new RuntimeException('Article row is not object');
    $allKeys[] = array_keys($row);
    if ($index === 0) {
        foreach ($row as $key => $fieldValue) $firstRowTypes[$key] = gettype($fieldValue);
    }
    foreach (['id', 'uuid', 'article_cate_uuid', 'article_cate_name', 'tag',
        'keyword', 'title', 'summary', 'content', 'status', 'create_time',
        'create_by', 'source_img'] as $field) {
        if (!array_key_exists($field, $row) || !array_key_exists($field, $source)) {
            throw new RuntimeException('Article source field missing: ' . $field);
        }
        if ($source[$field] === null) {
            if ($row[$field] !== null) {
                throw new RuntimeException('Article null source mismatch: ' . $field);
            }
        } elseif ((string)$row[$field] !== $source[$field]) {
            throw new RuntimeException('Article source value mismatch: ' . $field);
        }
        $types[$field][gettype($row[$field])] = true;
    }
    if (!array_key_exists('img', $row) || !is_string($row['img'])) {
        throw new RuntimeException('Article image type mismatch');
    }
    $path = str_replace('\\', '/', (string)($source['img'] ?? ''));
    if ($path !== '' && substr($row['img'], -strlen($path)) !== $path) {
        throw new RuntimeException('Article image source mismatch');
    }
    $types['img'][gettype($row['img'])] = true;
}
$articlesAfter = $fetchArticles();
$categoriesAfter = $fetchCategories();
$afterSha = hash('sha256', serialize([$articlesAfter, $categoriesAfter]));
if (!hash_equals($sourceSha, $afterSha)) {
    throw new RuntimeException('Article source changed during read-only HTTP');
}
foreach ($types as $key => $set) $types[$key] = array_keys($set);
$result += [
    'http_executed' => true, 'route' => $method . ' /m/controlScreen/articleSnapshot',
    'article_rows' => count($articlesBefore),
    'category_rows' => count($categoriesBefore),
    'source_sha' => $sourceSha, 'source_unchanged' => true,
    'http_ms' => $httpMs,
    'response_bytes' => strlen($body),
    'response_sha' => hash('sha256', $body),
    'row_key_signature_sha' => hash('sha256', serialize($allKeys)),
    'first_row_key_sha' => hash('sha256', serialize($allKeys[0] ?? [])),
    'first_row_types' => $firstRowTypes,
    'json_types' => $types,
];
echo json_encode($result, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), PHP_EOL;
