<?php
declare(strict_types=1);

// B/C isolated HTTP regression for a natural spicy_level=0 dish with components.
$lane = (string)getenv('CDX_SPICY_LANE');
$action = (string)($argv[1] ?? '');
$targets = [
    'B' => ['/tmp/zhct-http-store-mysql.dQTTBP', 'http://127.0.0.1:18081', 10001,
        'mysql', '003eb5aecf1cd4595b609c1f7f20b9ee325b578309fb9fe14fedaf77584fa401'],
    'C' => ['/tmp/zhct-dm-poc-20260928.fcpiiN/store', 'http://127.0.0.1:18080', 112175,
        'dm', 'bc98485aed26beab348770a82ae38dbf922b04ab88a210cd7282c4caf0156194'],
];
if (!isset($targets[$lane]) || !in_array($action, ['--preflight', '--execute'], true)
    || ($action === '--execute' && ((string)getenv('CDX_SPICY_EXECUTE') !== 'ROOT_WINDOW_APPROVED'
        || (string)getenv('CDX_SPICY_SHA') !== hash_file('sha256', __FILE__)))) {
    fwrite(STDERR, "execution disabled\n");
    exit(78);
}
[$root, $base, $pid, $dialect, $expectedIdentity] = $targets[$lane];
$hashes = [
    '/application/p/controller/Cater.php' => '9dc5a13d39ffa7b9d6f6909d5321348fc0fed56292ddd5e8fa12ce371bc0ecf2',
    '/application/p/logic/Cater.php' => '5d4ac22f89f8eb77ee322140c4726b431f8901bd7a4a462fed8d8ceff2a5ff09',
    '/application/common/service/DecimalResponseFormatter.php' => 'cce23d98388321732faded562fd212c609f1e0160c116de537aa0d5becaa437a',
];
if (is_link($root) || !is_dir($root)) throw new RuntimeException('Source root changed');
foreach ($hashes as $file => $sha) {
    if (hash_file('sha256', $root . $file) !== $sha) throw new RuntimeException('Cater source changed');
}
$workerEnvironment = (string)@file_get_contents('/proc/' . $pid . '/environ');
$ready = static function () use ($root, $base, $pid, $workerEnvironment): bool {
    $cmd = (string)@file_get_contents('/proc/' . $pid . '/cmdline');
    $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);
    return strpos($cmd, '127.0.0.1:' . $port) !== false
        && strpos($cmd, $root . '/public') !== false
        && $workerEnvironment !== '' && trim($children) === ''
        && !preg_match('/(?:^|\x00)PHP_CLI_SERVER_WORKERS=([2-9]|\d{2,})(?:\x00|$)/', $workerEnvironment)
        && is_array($boundary) && !empty($boundary['external_stubs'])
        && !empty($boundary['redis_isolated']) && empty($boundary['real_queue']);
};
if (!$ready()) throw new RuntimeException('Isolated worker boundary absent');
if ($dialect === 'dm') {
    foreach (explode("\0", $workerEnvironment) as $pair) {
        if (preg_match('/^(POC_[A-Z0-9_]+)=(.*)$/sD', $pair, $m)) putenv($m[1] . '=' . $m[2]);
    }
}
$config = require $root . '/application/database.php';
if (!is_array($config) || (string)($config['prefix'] ?? '') !== 'ydy_') {
    throw new RuntimeException('Database config shape mismatch');
}
if ($dialect === 'mysql') {
    $local = require '/workspace/zhct/zhctproject/store/application/config/local/database.php';
    if ((string)($config['type'] ?? '') !== 'mysql') throw new RuntimeException('Not MySQL');
    foreach (['hostname', 'database', 'username', 'password'] as $key) {
        if (($local[$key] ?? null) !== ($config[$key] ?? null)) throw new RuntimeException('Not code-local dev MySQL');
    }
    $dsn = 'mysql:host=' . $config['hostname'] . ';port=' . (int)($config['hostport'] ?? 3306)
        . ';dbname=' . $config['database'] . ';charset=' . (string)($config['charset'] ?? 'utf8');
} else {
    if ((string)($config['type'] ?? '') !== 'app\\cores\\database\\Dm'
        || (string)($config['hostname'] ?? '') !== '127.0.0.1'
        || (int)($config['hostport'] ?? 0) !== 5237
        || strtoupper((string)($config['database'] ?? '')) !== 'ZHCT_APP'
        || strtoupper((string)($config['username'] ?? '')) !== 'ZHCT_APP') {
        throw new RuntimeException('Not nonproduction DM');
    }
    $hostMark = hash('sha256', gethostname() . '|'
        . trim((string)@file_get_contents('/etc/machine-id')));
    if ($hostMark !== 'd56d81988bb0f106946cce91c608fdd0ef1116e5150b23376ea87e086eace4b9') {
        throw new RuntimeException('DM host mismatch');
    }
    $dsn = 'dm:host=127.0.0.1;port=5237;schema=ZHCT_APP';
}
$connect = static function () use ($dsn, $config): PDO {
    return new PDO($dsn, (string)$config['username'], (string)$config['password'],
        [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_CASE => PDO::CASE_LOWER,
            PDO::ATTR_TIMEOUT => 5]);
};
$db = $connect();
$identity = $dialect === 'mysql'
    ? (string)$db->query("SELECT CONCAT(@@server_uuid,'|',@@hostname,'|',@@port,'|',DATABASE())")->fetchColumn()
    : 'ZHCT_APP|5237|' . (string)$db->query(
        "SELECT SYS_CONTEXT('USERENV','CURRENT_SCHEMA') FROM DUAL")->fetchColumn();
if (!hash_equals($expectedIdentity, hash('sha256', $identity))) {
    throw new RuntimeException('Database identity mismatch');
}
$prefix = $dialect === 'dm' ? 'YDY_' : 'ydy_';
if ($dialect === 'mysql') {
    $column = $db->query("SELECT IS_NULLABLE nullable, COLUMN_DEFAULT default_value, DATA_TYPE data_type"
        . " FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE()"
        . " AND TABLE_NAME='ydy_dishes' AND COLUMN_NAME='spicy_level'")->fetch(PDO::FETCH_ASSOC);
} else {
    $column = $db->query("SELECT NULLABLE nullable, DATA_DEFAULT default_value, DATA_TYPE data_type"
        . " FROM USER_TAB_COLUMNS WHERE TABLE_NAME='YDY_DISHES' AND COLUMN_NAME='SPICY_LEVEL'")->fetch(PDO::FETCH_ASSOC);
}
if (!is_array($column)) throw new RuntimeException('Spicy column absent');
$nullCount = (int)$db->query('SELECT COUNT(*) FROM ' . $prefix . 'dishes WHERE SPICY_LEVEL IS NULL')->fetchColumn();
if ($dialect === 'mysql' && ((string)$column['nullable'] !== 'NO'
    || (string)$column['default_value'] !== '0' || $nullCount !== 0)) {
    throw new RuntimeException('B spicy migration gate failed');
}
$candidateSql = 'SELECT d.ID,d.UUID,d.SPICY_LEVEL,COUNT(c.ID) COMPONENT_COUNT'
    . ' FROM ' . $prefix . 'dishes d JOIN ' . $prefix . 'dish c'
    . ' ON c.DISHES_UUID=d.UUID AND c.COMPOSITION_RATIO>10'
    . ' WHERE d.IS_DEL=1 AND d.SPICY_LEVEL=0'
    . ' GROUP BY d.ID,d.UUID,d.SPICY_LEVEL HAVING COUNT(c.ID) BETWEEN 1 AND 5'
    . ' ORDER BY d.ID';
$candidateStmt = $db->query($candidateSql);
$candidate = $candidateStmt->fetch(PDO::FETCH_ASSOC);
$candidateStmt->closeCursor();
if (!is_array($candidate) || (int)$candidate['component_count'] < 1) {
    throw new RuntimeException('No natural spicy-zero dish with components');
}
$unique = $db->prepare('SELECT COUNT(*) FROM ' . $prefix . 'dishes WHERE UUID=? AND IS_DEL=1');
$unique->execute([(string)$candidate['uuid']]);
if ((int)$unique->fetchColumn() !== 1) throw new RuntimeException('Dish UUID not unique');
$read = static function (PDO $pdo) use ($prefix, $candidate): array {
    $s = $pdo->prepare('SELECT d.ID,d.UUID,d.SPICY_LEVEL,c.ID COMPONENT_ID,c.COMPOSITION_RATIO'
        . ' FROM ' . $prefix . 'dishes d JOIN ' . $prefix . 'dish c'
        . ' ON c.DISHES_UUID=d.UUID AND c.COMPOSITION_RATIO>10'
        . ' WHERE d.ID=? AND d.UUID=? AND d.IS_DEL=1 ORDER BY c.ID');
    $s->execute([(int)$candidate['id'], (string)$candidate['uuid']]);
    return $s->fetchAll(PDO::FETCH_ASSOC);
};
$source = $read($db);
if (count($source) !== (int)$candidate['component_count']) {
    throw new RuntimeException('Source components changed');
}
$sourceHash = hash('sha256', serialize($source));
$markerCount = (int)$db->query('SELECT COUNT(*) FROM ' . $prefix . 'user'
    . " WHERE ACCOUNT LIKE 'CDXSPY5%'")->fetchColumn();
if ($action === '--preflight') {
    echo json_encode(['lane' => $lane, 'mode' => 'SELECT-only',
        'column' => ['nullable' => $column['nullable'],
            'default_is_zero' => trim((string)$column['default_value'], " '()") === '0',
            'data_type' => $column['data_type'], 'null_count' => $nullCount],
        'candidate_id_sha12' => substr(hash('sha256', (string)$candidate['id']), 0, 12),
        'candidate_spicy_zero' => (float)$candidate['spicy_level'] === 0.0,
        'component_count' => count($source), 'prior_actor_count' => $markerCount]) . PHP_EOL;
    exit(0);
}
if ($markerCount !== 0) throw new RuntimeException('Preexisting actor marker');
$marker = 'CDXSPY5' . strtoupper(bin2hex(random_bytes(8)));
$dir = sys_get_temp_dir() . '/cdx-cater-spicy-' . $marker;
umask(0077);
if (!mkdir($dir, 0700)) throw new RuntimeException('Private journal directory unavailable');
$journal = $dir . '/recovery.json';
$operatorId = 0;
$httpSent = false;
$httpKnown = false;
$result = [];
$failure = '';
$record = ['lane' => $lane, 'marker' => $marker, 'operator_id' => 0,
    'database_fingerprint' => $expectedIdentity, 'phase' => 'before_insert'];
$save = static function (array $delta) use (&$record, $journal): void {
    $record = array_merge($record, $delta);
    if (file_put_contents($journal, json_encode($record)) === false) {
        throw new RuntimeException('Recovery journal write failed');
    }
    chmod($journal, 0600);
};
$save([]);
try {
    $insert = $db->prepare('INSERT INTO ' . $prefix . 'user'
        . ' (UUID,ACCOUNT,PWD,SALT,NAME,TYPE,CREATE_TIME,CREATE_BY,IS_DELETE)'
        . ' VALUES (?,?,?,?,?,?,?,?,?)');
    $insert->execute([$marker, $marker, 'DISABLED', strtoupper(bin2hex(random_bytes(8))),
        $marker, 0, date('Y-m-d H:i:s'), 'CODEX', 0]);
    $idQuery = $db->prepare('SELECT ID FROM ' . $prefix . 'user'
        . ' WHERE UUID=? AND ACCOUNT=? AND TYPE=0 AND CREATE_BY=?');
    $idQuery->execute([$marker, $marker, 'CODEX']);
    $operatorId = (int)$idQuery->fetchColumn();
    if ($operatorId <= 0) throw new RuntimeException('Actor insert unreadable');
    $save(['operator_id' => $operatorId, 'phase' => 'actor_inserted']);
    require_once $root . '/application/base/Security.php';
    $token = \app\base\Security::encrypt(implode("\t",
        [(string)$operatorId, $marker, $marker, '0', '', '', $marker]));
    if (!is_string($token) || $token === '' || !$ready()) {
        throw new RuntimeException('Token or worker unavailable');
    }
    $ctx = stream_context_create(['http' => ['method' => 'POST',
        'header' => "Content-Type: application/x-www-form-urlencoded\r\n"
            . 'token: ' . $token . "\r\n",
        'content' => http_build_query(['uuid' => (string)$candidate['uuid'], 'flag' => 0],
            '', '&', PHP_QUERY_RFC3986), 'timeout' => 25, 'ignore_errors' => true]]);
    $httpSent = true;
    $stream = @fopen($base . '/p/cater/find', 'rb', false, $ctx);
    $body = is_resource($stream) ? stream_get_contents($stream, 256 * 1024 + 1) : false;
    $meta = is_resource($stream) ? stream_get_meta_data($stream) : [];
    if (is_resource($stream)) fclose($stream);
    if (!is_string($body) || !empty($meta['timed_out']) || strlen($body) > 256 * 1024) {
        throw new RuntimeException('HTTP outcome incomplete');
    }
    $httpKnown = true;
    $headers = $http_response_header ?? [];
    preg_match('/HTTP\/\S+\s+(\d+)/', (string)($headers[0] ?? ''), $statusMatch);
    $httpStatus = (int)($statusMatch[1] ?? 0);
    $response = json_decode($body, true);
    $dish = is_array($response) ? ($response['data'] ?? null) : null;
    $components = is_array($dish) ? ($dish['data'] ?? null) : null;
    $componentIds = is_array($components) ? array_map(static function ($item) {
        return (int)($item['id'] ?? 0);
    }, $components) : [];
    $firstComponent = is_array($components) && isset($components[0]) ? $components[0] : [];
    $twoScale = is_array($dish) && isset($dish['weight'])
        && is_string($dish['weight'])
        && preg_match('/^-?\d+\.\d{2}$/D', $dish['weight']) === 1
        && is_array($firstComponent)
        && isset($firstComponent['composition_ratio'])
        && is_string($firstComponent['composition_ratio'])
        && preg_match('/^-?\d+\.\d{2}$/D', $firstComponent['composition_ratio']) === 1;
    $sourceIds = array_map(static function ($item) { return (int)$item['component_id']; }, $source);
    sort($componentIds);
    sort($sourceIds);
    $matched = $httpStatus === 200 && is_array($response)
        && ($response['code'] ?? null) === 0 && is_array($dish)
        && (int)($dish['id'] ?? 0) === (int)$candidate['id']
        && (string)($dish['uuid'] ?? '') === (string)$candidate['uuid']
        && is_int($dish['spicy_level'] ?? null) && $dish['spicy_level'] === 0
        && count($componentIds) === count($sourceIds) && $componentIds === $sourceIds
        && $twoScale;
    $result = ['lane' => $lane, 'http' => $httpStatus,
        'code' => is_array($response) ? ($response['code'] ?? null) : null,
        'spicy_level_type' => is_array($dish) ? gettype($dish['spicy_level'] ?? null) : 'missing',
        'spicy_level_zero' => is_array($dish) && ($dish['spicy_level'] ?? null) === 0,
        'weight_type' => is_array($dish) ? gettype($dish['weight'] ?? null) : 'missing',
        'component_ratio_type' => is_array($firstComponent)
            ? gettype($firstComponent['composition_ratio'] ?? null) : 'missing',
        'two_decimal_string_contract' => $twoScale,
        'component_count' => count($componentIds),
        'component_ids_match_source' => $componentIds === $sourceIds,
        'body_sha256' => hash('sha256', $body)];
    if (!$matched) throw new RuntimeException('Cater find source or JSON type mismatch');
    if (hash('sha256', serialize($read($connect()))) !== $sourceHash) {
        throw new RuntimeException('Dish or components changed');
    }
    $result['source_unchanged'] = true;
} catch (Throwable $e) {
    $failure = get_class($e) . ':' . hash('sha256', $e->getMessage());
} finally {
    if (!$httpSent || $httpKnown) {
        try {
            $fresh = $connect();
            $verify = $fresh->prepare('SELECT COUNT(*) FROM ' . $prefix . 'user'
                . ' WHERE ID=? AND UUID=? AND ACCOUNT=? AND TYPE=0 AND CREATE_BY=?');
            $verify->execute([$operatorId, $marker, $marker, 'CODEX']);
            if ((int)$verify->fetchColumn() === 1) {
                $delete = $fresh->prepare('DELETE FROM ' . $prefix . 'user'
                    . ' WHERE ID=? AND UUID=? AND ACCOUNT=? AND TYPE=0 AND CREATE_BY=?');
                $delete->execute([$operatorId, $marker, $marker, 'CODEX']);
                if ($delete->rowCount() !== 1) throw new RuntimeException('Precise cleanup failed');
            }
            $verify->execute([$operatorId, $marker, $marker, 'CODEX']);
            if ((int)$verify->fetchColumn() !== 0) throw new RuntimeException('Actor remains');
            $save(['phase' => 'cleaned']);
            $result['actor_cleaned'] = true;
            unlink($journal);
            rmdir($dir);
        } catch (Throwable $e) {
            $failure .= '|cleanup:' . get_class($e) . ':' . hash('sha256', $e->getMessage());
        }
    } else {
        $save(['phase' => 'HTTP_outcome_unknown']);
        $failure .= '|HTTP_outcome_unknown_actor_retained';
    }
}
echo json_encode(['result' => $result, 'failure' => $failure], JSON_UNESCAPED_SLASHES) . PHP_EOL;
if ($failure !== '') exit(1);
