<?php
declare(strict_types=1);

// B/C only. Exact three business GETs; original tables are SELECT-only.
// The sole mutation is a short isolated File-cache login token, deleted in finally.
$phase = (string)($argv[1] ?? '');
$lane = (string)($argv[2] ?? '');
if (!in_array($phase, ['--probe', '--run'], true)
    || !in_array($lane, ['B', 'C'], true)
    || ($phase === '--run'
        && (string)getenv('CDX_DELIVERY_READ3_RUN') !== 'ISOLATED_THREE_GET_V1')) {
    fwrite(STDERR, "DENIED: exact phase, lane and execution gate required\n");
    exit(78);
}

$GLOBALS['read3_stage'] = 'setup';
set_exception_handler(static function (Throwable $e): void {
    fwrite(STDERR, 'READ3_FAILED stage=' . $GLOBALS['read3_stage']
        . ' class=' . get_class($e) . "\n");
    exit(3);
});

$root = $lane === 'B'
    ? '/private/tmp/zhct-http-ai-macos.x2CCZX'
    : '/tmp/zhct-ai-http-20260929.3XDj3w';
$port = $lane === 'B' ? 18101 : 18102;
$pid = $lane === 'B' ? 13127 : 53772;
$base = 'http://127.0.0.1:' . $port;
if (!is_dir($root) || !is_file($root . '/vendor/autoload.php')
    || !is_file($root . '/public/index.php') || !is_file($root . '/env.php')) {
    throw new RuntimeException('Pinned HTTP docroot missing');
}
if (hash_file('sha256', $root . '/app/common/model/DeliveryRule.php')
    !== '3413695d32e92adef05d3512db2060ff96da49519200f140e16286bdf7dbed09') {
    throw new RuntimeException('DeliveryRule response fix source drift');
}
require_once $root . '/vendor/autoload.php';
$actualModel = (new ReflectionClass(\app\common\model\DeliveryRule::class))
    ->getFileName();
$expectedModel = $lane === 'B'
    ? '/Users/wiscourper/Documents/workspace/local/dm8-full-acceptance.fF5yTy/ai_api/app/common/model/DeliveryRule.php'
    : '/tmp/zhct-dm-poc-20260928.fcpiiN/ai_api/app/common/model/DeliveryRule.php';
if (realpath((string)$actualModel) !== $expectedModel
    || hash_file('sha256', (string)$actualModel)
        !== '3413695d32e92adef05d3512db2060ff96da49519200f140e16286bdf7dbed09') {
    throw new RuntimeException('Composer effective DeliveryRule source drift');
}

if ($lane === 'B') {
    $GLOBALS['read3_stage'] = 'B-worker-identity';
    $local = parse_ini_file(
        '/Users/wiscourper/Documents/workspace/zhct/zhctproject/ai_api/.env.local',
        true, INI_SCANNER_RAW);
    if (!is_array($local) || !isset($local['DATABASE'], $local['DATABASE_ZHCT'])) {
        throw new RuntimeException('B development config missing');
    }
    $cfg = $local['DATABASE'];
    if (strtolower((string)($cfg['TYPE'] ?? '')) !== 'mysql'
        || (string)($cfg['HOSTPORT'] ?? '') !== '3306') {
        throw new RuntimeException('B not code-local MySQL');
    }
    $ps = (string)shell_exec('/bin/ps eww -p ' . $pid . ' -o command=');
    $owners = trim((string)shell_exec('/usr/sbin/lsof -nP -t -iTCP:'
        . $port . ' -sTCP:LISTEN 2>/dev/null'));
    $has = static function (string $key, string $value) use ($ps): bool {
        return $value !== '' && strpos($ps, $key . '=' . $value) !== false;
    };
    foreach (['HOSTNAME' => 'HOST', 'HOSTPORT' => 'PORT',
        'DATABASE' => 'SCHEMA', 'USERNAME' => 'USER',
        'PASSWORD' => 'PASSWORD'] as $from => $to) {
        if (!$has('POC_AI_' . $to, (string)($cfg[$from] ?? ''))) {
            throw new RuntimeException('B worker/config identity drift');
        }
    }
    if ($owners !== (string)$pid
        || strpos($ps, 'POC_DIALECT=mysql') === false
        || strpos($ps, 'POC_EXTERNAL_STUBS=1') === false
        || (strpos($ps, $root . '/public') === false
            && strpos($ps, str_replace('/private/tmp/', '/tmp/', $root)
                . '/public') === false)
        || hash_file('sha256', $root . '/config/database.php')
            !== '8c46a6d9cd277f2950818d09162f639a62f135a5eb6553e6c61ce2c19167fd02'
        || hash_file('sha256', $root . '/config/cache.php')
            !== '98fa13484feb3db94bc4ebdd3de2f0b4ebc2c80d1cccb7a283a16f45eb9095a4'
        || hash_file('sha256', $root . '/public/index.php')
            !== '87ff41c14c0380cfba8cb99b02c8e440ad90f975e2e129dfb6e5657a36a7868d'
        || hash_file('sha256', $root . '/env.php')
            !== 'c1632f9105e355be219a1a0f3723954f968f312b8c3789c3aca7120a8160c27d') {
        throw new RuntimeException('B isolated worker drift');
    }
    $GLOBALS['read3_stage'] = 'B-database-connect';
    $db = new PDO('mysql:host=' . $cfg['HOSTNAME']
        . ';port=' . $cfg['HOSTPORT'] . ';dbname=' . $cfg['DATABASE']
        . ';charset=utf8', $cfg['USERNAME'], $cfg['PASSWORD'],
        [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_CASE => PDO::CASE_LOWER]);
    $deliveryTable = 'yoshop_delivery';
    $ruleTable = 'yoshop_delivery_rule';
} else {
    $GLOBALS['read3_stage'] = 'C-worker-identity';
    $env = [];
    foreach (explode("\0", (string)file_get_contents('/proc/' . $pid . '/environ'))
        as $item) {
        $pair = explode('=', $item, 2);
        if (count($pair) === 2) $env[$pair[0]] = $pair[1];
    }
    if (($env['POC_TARGET_ROOT'] ?? '') !== $root
        || ($env['POC_BASE_URL'] ?? '') !== $base
        || ($env['POC_DIALECT'] ?? '') !== 'dm'
        || ($env['POC_EXTERNAL_STUBS'] ?? '') !== '1'
        || ($env['POC_AI_HOST'] ?? '') !== '127.0.0.1'
        || ($env['POC_AI_PORT'] ?? '') !== '5238'
        || ($env['POC_AI_SCHEMA'] ?? '') !== 'AIZHCT_APP'
        || ($env['POC_AI_USER'] ?? '') !== 'AIZHCT_APP'
        || !in_array('dm', PDO::getAvailableDrivers(), true)) {
        throw new RuntimeException('C isolated worker identity drift');
    }
    $GLOBALS['read3_stage'] = 'C-database-connect';
    $db = new PDO('dm:host=127.0.0.1;port=5238;schema=AIZHCT_APP',
        $env['POC_AI_USER'], $env['POC_AI_PASSWORD'],
        [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_CASE => PDO::CASE_LOWER]);
    if ($db->query("SELECT SYS_CONTEXT('USERENV','CURRENT_SCHEMA') FROM DUAL")
        ->fetchColumn() !== 'AIZHCT_APP') {
        throw new RuntimeException('C effective schema drift');
    }
    $deliveryTable = 'YOSHOP_DELIVERY';
    $ruleTable = 'YOSHOP_DELIVERY_RULE';
}

$GLOBALS['read3_stage'] = 'HTTP-boundary';
$boundaryRaw = @file_get_contents($base . '/__poc_boundary', false,
    stream_context_create(['http' => ['timeout' => 5]]));
$boundary = is_string($boundaryRaw) ? json_decode($boundaryRaw, true) : null;
if (!is_string($boundaryRaw ?? null) && $lane === 'B') {
    throw new RuntimeException('B HTTP boundary unavailable');
}
if ($lane === 'C' && (!is_array($boundary) || empty($boundary['external_stubs']))) {
    throw new RuntimeException('Isolated HTTP boundary unavailable');
}
unset($boundaryRaw);

$snapshot = static function () use ($db, $deliveryTable, $ruleTable): array {
    $parents = $db->query('SELECT * FROM ' . $deliveryTable
        . ' WHERE STORE_ID=10001 AND IS_DELETE=0 ORDER BY DELIVERY_ID')
        ->fetchAll(PDO::FETCH_ASSOC);
    if (count($parents) < 1 || count($parents) > 15) {
        throw new RuntimeException('Natural delivery source absent or unbounded');
    }
    $ids = array_map(static function (array $row): int {
        return (int)$row['delivery_id'];
    }, $parents);
    if (min($ids) <= 0) throw new RuntimeException('Natural delivery ID invalid');
    $children = [];
    $stmt = $db->prepare('SELECT * FROM ' . $ruleTable
        . ' WHERE DELIVERY_ID=? ORDER BY RULE_ID');
    foreach ($ids as $id) {
        $stmt->execute([$id]);
        $children[(string)$id] = $stmt->fetchAll(PDO::FETCH_ASSOC);
    }
    $digest = hash('sha256', json_encode([$parents, $children],
        JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
    return ['ids' => $ids, 'rules' => $children, 'digest' => $digest];
};

$GLOBALS['read3_stage'] = 'original-source';
$first = $snapshot();
if ($phase === '--probe') {
    echo json_encode(['lane' => $lane, 'runtime_identity' => true,
        'external_stubs' => true, 'natural_active_rows' => count($first['ids']),
        'selected_rule_rows' => count($first['rules'][(string)$first['ids'][0]]),
        'original_sha256' => $first['digest'], 'business_http' => 0,
        'business_dml' => 0], JSON_UNESCAPED_SLASHES), "\n";
    exit(0);
}

define('TP_ENV', $lane === 'B' ? 'local' : 'poc');
define('BUSINESS', 'aizhct');
define('ENTERPRISE_NO', '10001');
require_once $root . '/vendor/autoload.php';
(new \think\App($root))->setEnvName($lane === 'B' ? 'local' : 'poc')->initialize();
if ($lane === 'B' && config('cache.default') !== 'file') {
    throw new RuntimeException('B effective cache is not isolated File');
}

$token = 'CDXDELIVERY' . strtoupper(bin2hex(random_bytes(16)));
$actor = ['user' => [
    'store_user_id' => 2147480001,
    'store_id' => 10001,
    'user_name' => 'CDX_DELIVERY_TEST',
    'real_name' => 'CDX_DELIVERY_TEST',
    'is_super' => 1,
], 'store_id' => 10001, 'is_login' => true];
$shape = null;
$shape = static function ($value) use (&$shape) {
    if (is_array($value)) {
        if (array_values($value) === $value) {
            return ['@array' => count($value), '@item' => count($value)
                ? $shape($value[0]) : null];
        }
        $out = [];
        foreach ($value as $key => $item) $out[(string)$key] = $shape($item);
        ksort($out);
        return $out;
    }
    if (is_int($value)) return 'integer';
    if (is_float($value)) return 'double';
    if (is_bool($value)) return 'boolean';
    if (is_string($value)) return 'string';
    return 'null';
};
$request = static function (string $action, ?int $id, ?string $token)
    use ($base): array {
    $url = $base . '/index.php?s=/store/setting.delivery/' . $action;
    if ($id !== null) $url .= '&deliveryId=' . rawurlencode((string)$id);
    $headers = $token === null ? '' : 'Access-Token: ' . $token . "\r\n";
    $context = stream_context_create(['http' => ['method' => 'GET',
        'header' => $headers, 'timeout' => 15, 'ignore_errors' => true,
        'follow_location' => 0]]);
    $start = microtime(true);
    $raw = @file_get_contents($url, false, $context);
    preg_match('/HTTP\/\S+\s+(\d+)/', $http_response_header[0] ?? '', $m);
    $json = is_string($raw) ? json_decode($raw, true) : null;
    unset($raw);
    return ['http' => isset($m[1]) ? (int)$m[1] : 0,
        'json' => is_array($json) ? $json : null,
        'elapsed_ms' => (int)round((microtime(true) - $start) * 1000)];
};

$out = ['lane' => $lane, 'natural_active_rows' => count($first['ids']),
    'selected_rule_rows' => count($first['rules'][(string)$first['ids'][0]]),
    'original_sha256' => $first['digest'], 'routes' => [],
    'business_dml' => 0, 'cache_token_deleted' => false];
$issued = false;
try {
    if (!\think\facade\Cache::set($token, $actor, 300)
        || !\think\facade\Cache::has($token)) {
        throw new RuntimeException('Short File token unavailable');
    }
    $issued = true;
    $negative = $request('list', null, null);
    $out['no_auth_list'] = ['http' => $negative['http'],
        'status' => $negative['json']['status'] ?? null,
        'rejected' => $negative['json'] !== null
            && (int)($negative['json']['status'] ?? 200) !== 200];
    if (!$out['no_auth_list']['rejected']) {
        throw new RuntimeException('Unauthenticated list not rejected');
    }
    foreach (['list', 'all', 'detail'] as $action) {
        $result = $request($action,
            $action === 'detail' ? $first['ids'][0] : null, $token);
        $json = $result['json'];
        $data = is_array($json) ? ($json['data'] ?? null) : null;
        $payload = $action === 'detail'
            ? ($data['detail'] ?? null)
            : ($action === 'list' ? ($data['list']['data'] ?? null)
                : ($data['list'] ?? null));
        $items = $action === 'detail' ? [$payload] : $payload;
        $feeFormats = null;
        if ($action !== 'all' && is_array($items) && isset($items[0]['rule'][0])) {
            $firstRule = $items[0]['rule'][0];
            $feeFormats = [];
            foreach (['first_fee', 'additional_fee'] as $feeName) {
                $fee = $firstRule[$feeName] ?? null;
                $feeFormats[$feeName] = [
                    'json_type' => gettype($fee),
                    'two_decimal_places' => is_string($fee)
                        && preg_match('/^-?\d+\.\d{2}$/', $fee) === 1,
                    'fraction_nonzero' => is_string($fee)
                        && preg_match('/\.\d*[1-9]$/', $fee) === 1,
                ];
            }
        }
        $returnedIds = is_array($items) ? array_map(
            static function ($item): int {
                return is_array($item) ? (int)($item['delivery_id'] ?? 0) : 0;
            }, $items) : [];
        sort($returnedIds);
        $expectedIds = $first['ids'];
        sort($expectedIds);
        $positive = $result['http'] === 200 && is_array($json)
            && (int)($json['status'] ?? -1) === 200
            && ($action === 'detail'
                ? $returnedIds === [$first['ids'][0]]
                : $returnedIds === $expectedIds);
        $next = $snapshot();
        $unchanged = $first['digest'] === $next['digest'];
        $out['routes'][$action] = ['http' => $result['http'],
            'status' => is_array($json) ? ($json['status'] ?? null) : null,
            'elapsed_ms' => $result['elapsed_ms'],
            'returned_count' => count($returnedIds),
            'source_count' => $action === 'detail' ? 1 : count($expectedIds),
            'ids_match_source' => $positive,
            'source_unchanged' => $unchanged,
            'fee_formats' => $feeFormats,
            'json_type_shape' => is_array($json) ? $shape($json) : null];
        if (!$positive || !$unchanged) {
            throw new RuntimeException('Delivery HTTP, row or source contract failed');
        }
    }
} catch (Throwable $e) {
    $out['failure_class'] = get_class($e);
    $out['pass'] = false;
} finally {
    if ($issued) {
        \think\facade\Cache::delete($token);
        $out['cache_token_deleted'] = !\think\facade\Cache::has($token);
    }
    $last = $snapshot();
    $out['final_source_unchanged'] = $first['digest'] === $last['digest'];
}
$out['pass'] = ($out['pass'] ?? true)
    && $out['cache_token_deleted'] && $out['final_source_unchanged']
    && count($out['routes']) === 3;
echo json_encode($out, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), "\n";
exit($out['pass'] ? 0 : 3);
