<?php
declare(strict_types=1);

// One natural dish, real mini-program POST JSON; only a short isolated File token
// may be written. Never output dish/customer values, IDs, names, or credentials.
$phase = (string)($argv[1] ?? '');
$lane = (string)getenv('CDX_DISH_DETAIL_LANE');
if (!in_array($phase, ['--preflight', '--execute'], true)
    || !in_array($lane, ['A', 'B', 'C'], true)
    || (string)getenv('CDX_DISH_DETAIL_SHA') !== hash_file('sha256', __FILE__)
    || (string)getenv('CDX_DISH_DETAIL_APPROVED') !==
        ($phase === '--preflight' ? 'READONLY_PREFLIGHT_V1' : 'ONE_POST_ONLY_V1')) {
    echo json_encode(['status' => 'default_deny', 'http' => 0,
        'business_dml' => 0]), PHP_EOL;
    exit(78);
}
if (PHP_VERSION_ID < 70400 || PHP_VERSION_ID >= 70500) {
    throw new RuntimeException('PHP7.4 required');
}
$stage = 'fixed-runtime-and-source';
set_error_handler(static function (int $severity): bool {
    if (!(error_reporting() & $severity)) return false;
    throw new ErrorException('redacted warning', 0, $severity);
});
set_exception_handler(static function (Throwable $error) use (&$stage): void {
    echo json_encode(['status' => 'halt', 'stage' => $stage,
        'class' => get_class($error),
        'error_sha256' => hash('sha256', $error->getMessage())]), PHP_EOL;
    exit(3);
});

$specs = [
    'A' => ['root' => '/private/tmp/zhct-http-ai-macos.zjaU8h',
        'source' => '/Users/wiscourper/Documents/workspace/local/dm8-full-acceptance.fF5yTy/baseline-a-ai-api-V4.0.40',
        'pid' => 13109, 'port' => 18105, 'socket' => '0x8baeb09c9a6de52f',
        'database_sha' => 'e7402b1ad50fcf4205326d93dc22e4f491c5052c60fcbe36214cd4e07b55343c',
        'service_sha' => '5064f65a4a112f2b30bb450cd3c2ce5619a3558408aaa97b7796e01ada4973c5'],
    'B' => ['root' => '/private/tmp/zhct-http-ai-macos.x2CCZX',
        'source' => '/Users/wiscourper/Documents/workspace/local/dm8-full-acceptance.fF5yTy/ai_api',
        'pid' => 13127, 'port' => 18101, 'socket' => '0xc1b5193ecc2ef2dc',
        'database_sha' => '8c46a6d9cd277f2950818d09162f639a62f135a5eb6553e6c61ce2c19167fd02',
        'service_sha' => 'bd01bf587cdcd21301a2d793c91042f536e25dadb9a5ff1e56bb1635f1843302'],
    'C' => ['root' => '/tmp/zhct-ai-http-20260929.3XDj3w',
        'source' => '/tmp/zhct-dm-poc-20260928.fcpiiN/ai_api',
        'pid' => 53772, 'port' => 18102, 'socket' => '441267',
        'database_sha' => 'e7402b1ad50fcf4205326d93dc22e4f491c5052c60fcbe36214cd4e07b55343c',
        'service_sha' => 'bd01bf587cdcd21301a2d793c91042f536e25dadb9a5ff1e56bb1635f1843302'],
];
$spec = $specs[$lane];
if (($lane === 'C') !== (PHP_OS_FAMILY === 'Linux')
    || realpath($spec['root']) !== $spec['root']
    || !is_dir($spec['root'] . '/runtime/cache')
    || hash_file('sha256', $spec['root'] . '/config/database.php') !== $spec['database_sha']
    || hash_file('sha256', $spec['root'] . '/config/cache.php')
        !== 'ccf1c56eb4178f3eaad8775e26a27742b2cacf662b46fb058cbb91d23783b8c6'
    || hash_file('sha256', $spec['root'] . '/external_stubs.php')
        !== 'ed0ea9f23c70e504cf5331b7a95fe2a9fd6704beabf254a474efbe8ecb4d59a0'
    || hash_file('sha256', $spec['root'] . '/app/api/controller/Dishes.php')
        !== '5b385f8950238319626ea6d68d4b7f8569ffff7489f29bf55e5af4b728e604d2'
    || hash_file('sha256', $spec['source'] . '/app/store/service/dishes/Dishes.php')
        !== $spec['service_sha']
    || hash_file('sha256', $spec['source'] . '/app/store/model/dishes/Dishes.php')
        !== 'c064228097b23537e1dd4d841e1195c6317daf84dd127bc8715a9bb0ba57e511') {
    throw new RuntimeException('fixed POC code/config/stub drift');
}
$listener = $lane === 'C'
    ? (string)shell_exec("ss -H -ltnpe '( sport = :18102 )' 2>/dev/null")
    : (string)shell_exec('/usr/sbin/lsof -nP -iTCP:' . $spec['port']
        . ' -sTCP:LISTEN 2>/dev/null');
if (strpos($listener, (string)$spec['pid']) === false
    || strpos($listener, $spec['socket']) === false
    || ($lane === 'C'
        && @readlink('/proc/53772/fd/3') !== 'socket:[441267]')) {
    throw new RuntimeException('fixed POC listener ownership drift');
}
require_once $spec['root'] . '/vendor/autoload.php';
foreach ([
    \app\api\controller\Dishes::class => 'app/api/controller/Dishes.php',
    \app\store\service\dishes\Dishes::class => 'app/store/service/dishes/Dishes.php',
] as $class => $relative) {
    if (realpath((new ReflectionClass($class))->getFileName())
        !== realpath($spec['source'] . '/' . $relative)) {
        throw new RuntimeException('actual Composer source path drift');
    }
}
$stubFile = $spec['root'] . '/runtime/poc-stub-hits.log';
$stubBefore = is_file($stubFile) ? hash_file('sha256', $stubFile) : 'absent';

putenv('CDX_READ2_TARGET=' . $lane);
putenv('CDX_READ2_SCHEMA_PREFLIGHT=1');
require $lane === 'C'
    ? '/tmp/zhct-ai-read2-VgKO4CDY/read2_schema_preflight.php'
    : __DIR__ . '/../parallel-ai-next-read-safe4/read2_schema_preflight.php';
$ai->setAttribute(PDO::ATTR_CASE, PDO::CASE_LOWER);
$store->setAttribute(PDO::ATTR_CASE, PDO::CASE_LOWER);
$table = $dialect === 'dm' ? 'YOSHOP_DISHES' : 'yoshop_dishes';
$tail = $dialect === 'dm' ? 'FETCH FIRST 1 ROWS ONLY' : 'LIMIT 1';
$sql = 'SELECT ID FROM ' . $table
    . ' WHERE STORE_ID=10001 AND IS_DELETE=0 AND SOURCE IN (1,2)'
    . ' AND WEIGHT>0 AND ENERGY>0 AND CARBOHYDRATE>0 AND PROTEIN>0 AND FAT>0'
    . ' ORDER BY ID DESC ' . $tail;
$dishId = (int)$ai->query($sql)->fetchColumn();
if ($dishId <= 0) throw new RuntimeException('no bounded natural positive dish');
$readDish = static function () use ($ai, $table, $dishId): array {
    $stmt = $ai->prepare('SELECT * FROM ' . $table
        . ' WHERE ID=? AND STORE_ID=10001 AND IS_DELETE=0');
    $stmt->execute([$dishId]);
    $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
    if (count($rows) !== 1) throw new RuntimeException('natural dish source changed');
    return array_change_key_case($rows[0], CASE_LOWER);
};
$sourceBefore = $readDish();
$sourceSha = hash('sha256', json_encode($sourceBefore,
    JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
if ($phase === '--preflight') {
    echo json_encode(['status' => 'dish_detail_preflight_pass', 'lane' => $lane,
        'dish_id_sha256' => hash('sha256', (string)$dishId),
        'source_sha256' => $sourceSha, 'http' => 0, 'business_dml' => 0]), PHP_EOL;
    exit(0);
}

$stage = 'private-synthetic-token-and-one-POST';
$marker = $lane === 'C' ? 'CDXPARMOB9411C2CB' : 'CDXPARMOB426CA4B0';
$userTable = $dialect === 'dm' ? 'YOSHOP_USER' : 'yoshop_user';
$storeTable = $dialect === 'dm' ? 'YDY_USER' : 'ydy_user';
$getRows = static function (PDO $db, string $sql, array $args): array {
    $stmt = $db->prepare($sql);
    $stmt->execute($args);
    return $stmt->fetchAll(PDO::FETCH_ASSOC);
};
$userRows = $getRows($ai, 'SELECT * FROM ' . $userTable
    . ' WHERE NICK_NAME=? AND STORE_ID=10001 AND IS_DELETE=0', [$marker]);
if (count($userRows) !== 1 || (int)$userRows[0]['user_id'] <= 0) {
    throw new RuntimeException('synthetic AI actor missing or nonunique');
}
$userId = (int)$userRows[0]['user_id'];
$storeRows = $getRows($store, 'SELECT * FROM ' . $storeTable
    . ' WHERE AI_USER_ID=? AND IS_DELETE=0', [$userId]);
if (count($storeRows) !== 1 || !isset($storeRows[0]['password_version'])) {
    throw new RuntimeException('synthetic Store mapping/version missing');
}
$actorSha = hash('sha256', serialize([$userRows, $storeRows]));
foreach ($config as $key => $value) {
    if (strpos($key, 'POC_') === 0) putenv($key . '=' . $value);
}
define('TP_ENV', 'poc');
define('BUSINESS', 'aizhct');
define('ENTERPRISE_NO', '10001');
(new \think\App($spec['root']))->setEnvName('poc')->initialize();
if (strtolower((string)\think\facade\Config::get('cache.default')) !== 'file'
    || realpath((string)\think\facade\Config::get('cache.stores.file.path'))
        !== $spec['root'] . '/runtime/cache') {
    throw new RuntimeException('isolated File cache changed');
}
$model = \app\api\model\User::detail($userId);
if (!$model || (int)$model['user_id'] !== $userId) {
    throw new RuntimeException('synthetic login model unavailable');
}
$token = 'CDXREAD8T' . strtoupper(bin2hex(random_bytes(18)));
$journalDir = sys_get_temp_dir() . '/cdx-ai-read8-'
    . strtolower(bin2hex(random_bytes(8)));
if (!mkdir($journalDir, 0700) || !chmod($journalDir, 0700)) {
    throw new RuntimeException('private token journal unavailable');
}
$journal = $journalDir . '/token.json';
$manifest = ['target' => $lane, 'role' => 'dish_detail', 'root' => $spec['root'],
    'token' => $token, 'created_at' => time(), 'expires_at' => time() + 300];
if (file_put_contents($journal, json_encode($manifest), LOCK_EX) === false
    || !chmod($journal, 0600)) {
    throw new RuntimeException('private token journal write failed');
}
$result = ['lane' => $lane, 'journal' => $journal, 'source_sha256' => $sourceSha,
    'dish_id_sha256' => hash('sha256', (string)$dishId),
    'cache_cleanup' => 'not_started'];
$tokenSaved = false;
try {
    $tokenSaved = \think\facade\Cache::set($token, [
        'user' => $model, 'store_id' => 10001, 'is_login' => true,
        'password_version' => (int)$storeRows[0]['password_version'],
        'login_type' => 'sms',
    ], 300);
    if (!$tokenSaved || !\think\facade\Cache::has($token)) {
        throw new RuntimeException('short File token unavailable');
    }
    $body = json_encode(['id' => $dishId, 'storeId' => 10001]);
    $ctx = stream_context_create(['http' => [
        'method' => 'POST', 'timeout' => 15, 'follow_location' => 0,
        'ignore_errors' => true, 'content' => $body,
        'header' => 'Access-Token: ' . $token . "\r\n"
            . "platform: MP-WEIXIN\r\nversion: 2.0.51\r\n"
            . "env-version: develop\r\nContent-Type: application/json;charset=utf-8\r\n",
    ]]);
    $handle = @fopen('http://127.0.0.1:' . $spec['port']
        . '/api/Dishes/detail', 'rb', false, $ctx);
    if ($handle === false) throw new RuntimeException('one loopback POST unavailable');
    $raw = stream_get_contents($handle, 1048577);
    $meta = stream_get_meta_data($handle);
    fclose($handle);
    if (!is_string($raw) || strlen($raw) > 1048576 || !empty($meta['timed_out'])) {
        throw new RuntimeException('POST timeout or response cap');
    }
    $line = (string)(($meta['wrapper_data'] ?? [])[0] ?? '');
    $response = json_decode($raw, true);
    $result['http'] = preg_match('/^HTTP\/\S+\s+200\b/', $line) ? 200 : 0;
    $result['business_status'] = is_array($response) ? ($response['status'] ?? null) : null;
    $detail = is_array($response) ? ($response['data']['detail'] ?? null) : null;
    if ($result['http'] !== 200 || $result['business_status'] !== 200
        || !is_array($detail) || (int)($detail['id'] ?? 0) !== $dishId) {
        throw new RuntimeException('positive detail envelope or ownership failed');
    }
    $fields = ['weight', 'energy', 'carbohydrate', 'protein', 'fat'];
    $types = [];
    $valueMatches = [];
    foreach ($fields as $field) {
        if (!isset($detail[$field], $sourceBefore[$field])
            || !is_numeric($detail[$field])) {
            throw new RuntimeException('detail numeric source/response field missing');
        }
        $types[$field] = gettype($detail[$field]);
        if ($field === 'weight') {
            $valueMatches[$field] = abs((float)$detail[$field]
                - (float)$sourceBefore[$field]) < 0.000001;
        } else {
            $expected = \app\common\Algorithm::divScale(
                (float)$sourceBefore[$field] * 100, (float)$sourceBefore['weight']);
            $valueMatches[$field] = abs((float)$detail[$field] - $expected) < 0.000001;
        }
    }
    if (in_array(false, $valueMatches, true)) {
        throw new RuntimeException('detail weight/nutrients differ from own original row');
    }
    $result['selected_types'] = $types;
    $result['selected_types_sha256'] = hash('sha256', json_encode($types));
    $result['all_selected_values_match_source'] = true;
    $result['response_keys_sha256'] = hash('sha256', json_encode(array_keys($detail)));
    unset($response['trace_id']);
    $result['body_without_trace_sha256'] = hash('sha256',
        json_encode($response, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
} catch (Throwable $error) {
    $result['error_class'] = get_class($error);
    $result['error_sha256'] = hash('sha256', $error->getMessage());
} finally {
    if ($tokenSaved) {
        try {
            \think\facade\Cache::delete($token);
            $result['cache_cleanup'] = \think\facade\Cache::has($token)
                ? 'failed' : 'first_process_zero';
        } catch (Throwable $error) {
            $result['cache_cleanup'] = 'failed';
        }
    } else {
        $result['cache_cleanup'] = 'never_set';
    }
    try {
        $afterDish = $readDish();
        $afterUsers = $getRows($ai, 'SELECT * FROM ' . $userTable
            . ' WHERE USER_ID=? AND STORE_ID=10001', [$userId]);
        $afterStores = $getRows($store, 'SELECT * FROM ' . $storeTable
            . ' WHERE AI_USER_ID=? AND IS_DELETE=0', [$userId]);
        $result['source_and_actor_unchanged'] =
            hash('sha256', json_encode($afterDish,
                JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)) === $sourceSha
            && hash('sha256', serialize([$afterUsers, $afterStores])) === $actorSha
            && (is_file($stubFile) ? hash_file('sha256', $stubFile) : 'absent')
                === $stubBefore;
    } catch (Throwable $error) {
        $result['source_and_actor_unchanged'] = false;
    }
}
$result['status'] = !isset($result['error_class'])
    && $result['cache_cleanup'] === 'first_process_zero'
    && $result['source_and_actor_unchanged'] === true
    ? 'pending_independent_token_zero' : 'failed_or_deferred';
echo json_encode($result, JSON_UNESCAPED_SLASHES), PHP_EOL;
exit($result['status'] === 'pending_independent_token_zero' ? 0 : 3);
