<?php
declare(strict_types=1);

// DEFAULT DENY: own single-spec goods only. No upload, OSS, payment or orders.
$phase = (string)($argv[1] ?? '');
$lane = (string)($argv[2] ?? '');
$marker = (string)($argv[3] ?? '');
if (!in_array($phase, ['--preflight', '--execute', '--audit-token', '--cleanup', '--recover-partial', '--audit-cleanup'], true)
    || !in_array($lane, ['B', 'C'], true)
    || !preg_match('/^CDXGOODS[0-9A-F]{10}$/', $marker)) {
    fwrite(STDERR, "DENIED: exact phase/lane/marker required\n");
    exit(78);
}
if ($phase === '--recover-partial'
    && (string)getenv('CDX_GOODS_AE_RECOVER_APPROVAL') !== 'OWNED_ADD_EDIT_PARTIAL_RECOVERY_V1') {
    fwrite(STDERR, "DENIED: partial recovery requires separate approval\n");
    exit(78);
}
if ($phase !== '--preflight') {
    $reviewed = (string)getenv('CDX_GOODS_AE_REVIEWED_SHA256');
    if ((string)getenv('CDX_GOODS_AE_APPROVAL') !== 'OWNED_ADD_EDIT_SINGLE_V1'
        || !preg_match('/^[0-9a-f]{64}$/', $reviewed)
        || ($lane === 'B' && hash_file('sha256', __FILE__) !== $reviewed)
        || ($lane === 'C' && (string)getenv('CDX_GOODS_AE_SOURCE_CHECKED') !== '1')) {
        fwrite(STDERR, "DENIED: exact reviewed SHA/write gate required\n");
        exit(78);
    }
}
$stage = 'start';
set_exception_handler(static function (Throwable $e) use (&$stage): void {
    fwrite(STDERR, 'GOODS_AE_STOP stage=' . $stage . ' class=' . get_class($e) . "\n");
    exit(3);
});
$root = $lane === 'B' ? '/private/tmp/zhct-http-ai-macos.x2CCZX'
    : '/tmp/zhct-ai-http-20260929.3XDj3w';
$pid = $lane === 'B' ? 13127 : 53772;
$port = $lane === 'B' ? 18101 : 18102;
$base = 'http://127.0.0.1:' . $port;
if (!is_file($root . '/public/index.php') || !is_file($root . '/vendor/autoload.php')) {
    throw new RuntimeException('Isolated HTTP root absent');
}
if ($lane === 'B') {
    $cfg = parse_ini_file('/Users/wiscourper/Documents/workspace/zhct/zhctproject/ai_api/.env.local', true, INI_SCANNER_RAW)['DATABASE'] ?? null;
    $ps = (string)shell_exec('/bin/ps eww -p ' . $pid . ' -o command=');
    $owner = trim((string)shell_exec('/usr/sbin/lsof -nP -t -iTCP:' . $port . ' -sTCP:LISTEN 2>/dev/null'));
    if (!is_array($cfg) || strtolower((string)($cfg['TYPE'] ?? '')) !== 'mysql'
        || (string)($cfg['HOSTPORT'] ?? '') !== '3306' || $owner !== (string)$pid
        || strpos($ps, 'POC_DIALECT=mysql') === false || strpos($ps, 'POC_EXTERNAL_STUBS=1') === false) {
        throw new RuntimeException('B worker/config drift');
    }
    foreach (['HOSTNAME' => 'HOST', 'HOSTPORT' => 'PORT', 'DATABASE' => 'SCHEMA', 'USERNAME' => 'USER', 'PASSWORD' => 'PASSWORD'] as $from => $to) {
        $value = (string)($cfg[$from] ?? '');
        if ($value === '' || strpos($ps, 'POC_AI_' . $to . '=' . $value) === false) {
            throw new RuntimeException('B worker differs from code local config');
        }
    }
    $connect = static function () use ($cfg): PDO {
        return 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]);
    };
    $envName = 'local';
    $prefix = 'yoshop_';
} else {
    $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 worker/config drift');
    }
    $connect = static function () use ($env): PDO {
        $d = 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 ($d->query("SELECT SYS_CONTEXT('USERENV','CURRENT_SCHEMA') FROM DUAL")
            ->fetchColumn() !== 'AIZHCT_APP') throw new RuntimeException('C schema drift');
        return $d;
    };
    $envName = 'poc';
    $prefix = 'YOSHOP_';
}
$name = static function (string $suffix) use ($lane, $prefix): string {
    return $prefix . ($lane === 'B' ? $suffix : strtoupper($suffix));
};
$table = ['goods' => $name('goods'), 'sku' => $name('goods_sku'),
    'image' => $name('goods_image'), 'relation' => $name('goods_category_rel'),
    'category' => $name('category'), 'file' => $name('upload_file'),
    'spec' => $name('goods_spec_rel'), 'service' => $name('goods_service_rel'),
    'delivery' => $name('delivery')];
$db = $connect();
$rows = static function (PDO $d, string $sql, array $args = []): array {
    $s = $d->prepare($sql);
    $s->execute($args);
    return $s->fetchAll(PDO::FETCH_ASSOC);
};
$count = static function (PDO $d, string $sql, array $args = []): int {
    $s = $d->prepare($sql);
    $s->execute($args);
    return (int)$s->fetchColumn();
};
$productMarker = $marker . 'API';
$fixturePath = sys_get_temp_dir() . '/cdx-goods-' . strtolower($lane)
    . '-' . strtolower($marker) . '/journal.json';
$fixture = is_file($fixturePath) ? json_decode((string)file_get_contents($fixturePath), true) : null;
$fixtureReady = is_array($fixture) && ($fixture['lane'] ?? '') === $lane
    && ($fixture['marker'] ?? '') === $marker
    && ($fixture['phase'] ?? '') === 'fixture_committed';
$deliveryIds = $rows($db, 'SELECT DELIVERY_ID FROM ' . $table['delivery']
    . ' WHERE STORE_ID=10001 AND IS_DELETE=0 ORDER BY DELIVERY_ID');
$deliveryId = count($deliveryIds) ? (int)$deliveryIds[0]['delivery_id'] : 0;
$productRows = $rows($db, 'SELECT * FROM ' . $table['goods']
    . ' WHERE STORE_ID=10001 AND GOODS_NAME=?', [$productMarker]);
$journalDir = sys_get_temp_dir() . '/cdx-goods-ae-' . strtolower($lane)
    . '-' . strtolower($marker);
$journalPath = $journalDir . '/journal.json';
if ($phase === '--preflight') {
    echo json_encode(['lane' => $lane, 'phase' => $phase,
        'base_fixture_ready' => $fixtureReady,
        'active_delivery_count' => count($deliveryIds),
        'api_goods_marker_zero' => count($productRows) === 0,
        'journal_absent' => !file_exists($journalDir),
        'business_http' => 0, 'business_dml' => 0]), "\n";
    exit(0);
}
if (!$fixtureReady || (int)($fixture['ids']['category'] ?? 0) <= 0
    || (int)($fixture['ids']['file'] ?? 0) <= 0
    || (int)($fixture['ids']['goods'] ?? 0) <= 0) {
    throw new RuntimeException('Base fixture missing');
}
$baseIds = array_map('intval', $fixture['ids']);
$baseSnapshot = static function (PDO $d) use ($rows, $table, $baseIds): string {
    $pk = ['goods' => 'GOODS_ID', 'sku' => 'ID', 'image' => 'ID',
        'relation' => 'ID', 'category' => 'CATEGORY_ID', 'file' => 'FILE_ID'];
    $part = [];
    foreach ($pk as $logical => $key) {
        $r = $rows($d, 'SELECT * FROM ' . $table[$logical]
            . ' WHERE ' . $key . '=? AND STORE_ID=10001', [$baseIds[$logical]]);
        if (count($r) !== 1) throw new RuntimeException('Base row missing');
        $part[$logical] = $r[0];
    }
    return hash('sha256', serialize($part));
};
$baseSha = $baseSnapshot($db);
$capture = static function (PDO $d) use ($rows, $count, $table, $productMarker): array {
    $g = $rows($d, 'SELECT * FROM ' . $table['goods']
        . ' WHERE STORE_ID=10001 AND GOODS_NAME=?', [$productMarker]);
    if (count($g) !== 1) throw new RuntimeException('API goods row absent/ambiguous');
    $id = (int)$g[0]['goods_id'];
    if ($id <= 0) throw new RuntimeException('API goods identity invalid');
    $part = ['goods' => $g[0]];
    foreach (['sku', 'image', 'relation'] as $logical) {
        $r = $rows($d, 'SELECT * FROM ' . $table[$logical]
            . ' WHERE GOODS_ID=? AND STORE_ID=10001', [$id]);
        if (count($r) !== 1) throw new RuntimeException('API child row absent/ambiguous');
        $part[$logical] = $r[0];
    }
    foreach (['spec', 'service'] as $logical) {
        if ($count($d, 'SELECT COUNT(*) FROM ' . $table[$logical]
            . ' WHERE GOODS_ID=?', [$id]) !== 0) {
            throw new RuntimeException('Unexpected multi-spec/service relation');
        }
    }
    return $part;
};
$save = static function (array $value) use ($journalPath): void {
    $body = json_encode($value, JSON_UNESCAPED_SLASHES);
    $tmp = $journalPath . '.tmp';
    if (!is_string($body) || file_put_contents($tmp, $body, LOCK_EX) !== strlen($body)
        || !chmod($tmp, 0600) || !rename($tmp, $journalPath)) {
        throw new RuntimeException('Private recovery journal write failed');
    }
};
$journal = is_file($journalPath)
    ? json_decode((string)file_get_contents($journalPath), true) : null;
if ($phase !== '--execute') {
    if (!is_array($journal) || ($journal['lane'] ?? '') !== $lane
        || ($journal['marker'] ?? '') !== $marker
        || ($journal['product_marker'] ?? '') !== $productMarker
        || ($journal['base_sha256'] ?? '') !== $baseSha) {
        throw new RuntimeException('Recovery journal identity/source mismatch');
    }
}
// The remaining phases are below. No HTTP or DML before the exact approval gate.
if ($phase === '--audit-cleanup') {
    $stage = 'audit-cleanup';
    $id = (int)($journal['ids']['goods'] ?? 0);
    if (($journal['phase'] ?? '') !== 'cleanup_committed' || $id <= 0
        || count($productRows) !== 0 || $baseSnapshot($connect()) !== $baseSha) {
        throw new RuntimeException('Independent base/API zero not proven');
    }
    foreach (['sku', 'image', 'relation', 'spec', 'service'] as $logical) {
        if ($count($db, 'SELECT COUNT(*) FROM ' . $table[$logical]
            . ' WHERE GOODS_ID=?', [$id]) !== 0) {
            throw new RuntimeException('API child row remains');
        }
    }
    if (!unlink($journalPath) || !rmdir($journalDir)
        || file_exists($journalPath) || is_dir($journalDir)) {
        throw new RuntimeException('Exact journal removal failed');
    }
    echo json_encode(['lane' => $lane, 'phase' => $phase,
        'api_goods_zero' => true, 'api_children_zero' => true,
        'base_fixture_unchanged' => true, 'journal_absent' => true,
        'business_http' => 0, 'business_dml' => 0]), "\n";
    exit(0);
}
if ($phase === '--cleanup' || $phase === '--recover-partial') {
    $stage = 'cleanup-gate';
    $partial = $phase === '--recover-partial';
    $retainedPhase = (string)($journal['phase'] ?? '');
    if (($partial && !in_array($retainedPhase, ['added', 'before_edit'], true))
        || (!$partial && $retainedPhase !== 'completed')
        || ($journal['token_audited'] ?? false) !== true
        || count($productRows) !== 1) {
        throw new RuntimeException('Completed API product/token gate absent');
    }
    $p = $capture($db);
    $id = (int)$p['goods']['goods_id'];
    $fileId = $baseIds['file'];
    $categoryId = $baseIds['category'];
    foreach (['goods' => 'goods_id', 'relation' => 'id'] as $logical => $pk) {
        if ((int)$p[$logical][$pk] !== (int)($journal['ids'][$logical] ?? 0)) {
            throw new RuntimeException('API child identity/journal drift');
        }
    }
    $skuSame = (int)$p['sku']['id'] === (int)($journal['ids']['sku'] ?? 0);
    $imageSame = (int)$p['image']['id'] === (int)($journal['ids']['image'] ?? 0);
    $price = number_format((float)$p['goods']['goods_price_min'], 2, '.', '');
    $line = number_format((float)$p['sku']['line_price'], 2, '.', '');
    $point = (string)$p['goods']['selling_point'];
    $addedState = $skuSame && $imageSame && $price === '12.34'
        && $line === '15.00' && $point === $marker;
    $editedState = !$skuSame && !$imageSame && $price === '13.45'
        && $line === '17.00' && $point === $marker . 'EDIT';
    $completedState = $skuSame && $imageSame && $price === '13.45'
        && $line === '17.00' && $point === $marker . 'EDIT';
    if ((!$partial && !$completedState)
        || ($partial && $retainedPhase === 'added' && !$addedState)
        || ($partial && $retainedPhase === 'before_edit'
            && !$addedState && !$editedState)) {
        throw new RuntimeException('API product phase/price/child identity drift');
    }
    if ((int)$p['goods']['status'] !== 20 || (int)$p['goods']['stock_total'] !== 0
        || (int)$p['goods']['is_delete'] !== 0
        || (int)$p['image']['image_id'] !== $fileId
        || (int)$p['relation']['category_id'] !== $categoryId
        || (int)$p['sku']['image_id'] !== 0
        || $baseSnapshot($db) !== $baseSha) {
        throw new RuntimeException('API product/base cross-link drift');
    }
    $meta = $lane === 'B'
        ? "SELECT TABLE_NAME,COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME LIKE 'yoshop_%' AND UPPER(COLUMN_NAME) IN ('GOODS_ID','IMAGE_ID','FILE_ID','VIDEO_ID','VIDEO_COVER_ID')"
        : "SELECT TABLE_NAME,COLUMN_NAME FROM USER_TAB_COLUMNS WHERE TABLE_NAME LIKE 'YOSHOP_%' AND COLUMN_NAME IN ('GOODS_ID','IMAGE_ID','FILE_ID','VIDEO_ID','VIDEO_COVER_ID')";
    $refs = $rows($db, $meta);
    if (count($refs) > 250) throw new RuntimeException('Reference scan cap exceeded');
    foreach ($refs as $ref) {
        $t = (string)$ref['table_name'];
        $c = (string)$ref['column_name'];
        if (!preg_match('/^[A-Za-z_][A-Za-z0-9_]*$/', $t)
            || !preg_match('/^[A-Za-z_][A-Za-z0-9_]*$/', $c)) {
            throw new RuntimeException('Reference metadata unsafe');
        }
        if (strcasecmp($t, $table['goods']) === 0 && strcasecmp($c, 'GOODS_ID') === 0) continue;
        if (strcasecmp($t, $table['file']) === 0 && strcasecmp($c, 'FILE_ID') === 0) continue;
        $goodsRef = strcasecmp($c, 'GOODS_ID') === 0;
        $want = 0;
        if ($goodsRef && in_array(strtolower($t), [strtolower($table['sku']),
            strtolower($table['image']), strtolower($table['relation'])], true)) $want = 1;
        if (!$goodsRef && strcasecmp($c, 'IMAGE_ID') === 0) {
            if (strcasecmp($t, $table['image']) === 0) $want = 2; // base + API
            if (strcasecmp($t, $table['sku']) === 0) $want = 1; // base only
        }
        if ($count($db, 'SELECT COUNT(*) FROM ' . $t . ' WHERE ' . $c . '=?',
            [$goodsRef ? $id : $fileId]) !== $want) {
            throw new RuntimeException('Unexpected external reference; cleanup refused');
        }
    }
    if ($count($db, 'SELECT COUNT(*) FROM ' . $table['relation']
        . ' WHERE CATEGORY_ID=?', [$categoryId]) !== 2
        || $count($db, 'SELECT COUNT(*) FROM ' . $table['category']
        . ' WHERE PARENT_ID=?', [$categoryId]) !== 0) {
        throw new RuntimeException('Shared category reference drift');
    }
    $stage = 'exact-cleanup';
    $db->beginTransaction();
    try {
        foreach ([['image','ID','id'], ['sku','ID','id'],
            ['relation','ID','id'], ['goods','GOODS_ID','goods_id']]
            as [$logical, $sqlPk, $arrayPk]) {
            $s = $db->prepare('DELETE FROM ' . $table[$logical]
                . ' WHERE ' . $sqlPk . '=? AND STORE_ID=10001'
                . ' AND ' . ($logical === 'goods' ? 'GOODS_NAME=?' : 'GOODS_ID=?'));
            $s->execute([(int)$p[$logical][$arrayPk],
                $logical === 'goods' ? $productMarker : $id]);
            if ($s->rowCount() !== 1) throw new RuntimeException('Exact delete row count mismatch');
        }
        $db->commit();
    } catch (Throwable $e) {
        if ($db->inTransaction()) $db->rollBack();
        throw $e;
    }
    $journal['phase'] = 'cleanup_committed';
    $journal['ids']['sku'] = (int)$p['sku']['id'];
    $journal['ids']['image'] = (int)$p['image']['id'];
    if ($partial) $journal['recovered_from_phase'] = $retainedPhase;
    $save($journal);
    echo json_encode(['lane' => $lane, 'phase' => $phase,
        'deleted_exact_api_rows' => 4,
        'partial_recovery' => $partial,
        'base_fixture_retained' => true,
        'independent_audit_required' => true]), "\n";
    exit(0);
}
define('TP_ENV', $envName);
define('BUSINESS', 'aizhct');
define('ENTERPRISE_NO', '10001');
require_once $root . '/vendor/autoload.php';
(new \think\App($root))->setEnvName($envName)->initialize();
if (config('cache.default') !== 'file') throw new RuntimeException('Private File cache absent');
if ($phase === '--audit-token') {
    $stage = 'audit-token';
    $token = (string)($journal['token'] ?? '');
    if (!preg_match('/^CDXGOODSAE[0-9A-F]{32}$/', $token)) {
        throw new RuntimeException('Token journal value invalid');
    }
    $boundary = @fopen($base . '/__poc_boundary', 'rb', false,
        stream_context_create(['http' => ['timeout' => 5,
            'ignore_errors' => true, 'follow_location' => 0]]));
    if ($boundary === false) throw new RuntimeException('Isolated worker completion boundary absent');
    $boundaryBody = stream_get_contents($boundary, 8193);
    $boundaryMeta = stream_get_meta_data($boundary);
    $boundaryEof = feof($boundary);
    fclose($boundary);
    $boundaryJson = is_string($boundaryBody) ? json_decode($boundaryBody, true) : null;
    if (!is_string($boundaryBody) || strlen($boundaryBody) > 8192
        || !$boundaryEof || ($boundaryMeta['timed_out'] ?? false)
        || ($lane === 'C' && (!is_array($boundaryJson)
            || empty($boundaryJson['external_stubs'])))) {
        throw new RuntimeException('Isolated worker completion boundary uncertain');
    }
    if (\think\facade\Cache::has($token)) {
        throw new RuntimeException('Independent File token zero not proven');
    }
    if (($journal['phase'] ?? '') === 'completed') {
        $p = $capture($connect());
        if (hash('sha256', serialize($p)) !== ($journal['product_sha256'] ?? '')) {
            throw new RuntimeException('API product source changed after HTTP');
        }
    }
    $journal['token_audited'] = true;
    unset($journal['token']);
    $save($journal);
    echo json_encode(['lane' => $lane, 'phase' => $phase,
        'token_zero' => true, 'worker_boundary_confirmed' => true,
        'recovery_journal_retained' => true,
        'business_http' => 0, 'business_dml' => 0]), "\n";
    exit(0);
}
if ($phase !== '--execute' || $deliveryId <= 0 || count($productRows) !== 0
    || file_exists($journalDir) || !mkdir($journalDir, 0700)
    || !chmod($journalDir, 0700)) {
    throw new RuntimeException('API add preflight/journal not pristine');
}
$token = 'CDXGOODSAE' . strtoupper(bin2hex(random_bytes(16)));
$journal = ['lane' => $lane, 'marker' => $marker,
    'product_marker' => $productMarker, 'phase' => 'before_add',
    'base_ids' => $baseIds, 'base_sha256' => $baseSha,
    'delivery_id' => $deliveryId, 'token' => $token,
    'token_audited' => false, 'ids' => [], 'created_at' => time()];
$save($journal);
echo json_encode(['lane' => $lane, 'phase' => 'journal_ready',
    'journal' => $journalPath, 'business_http' => 0,
    'business_dml' => 0]), "\n";
flush();
$shape = null;
$shape = static function ($value) use (&$shape) {
    if (is_array($value)) {
        if (array_values($value) === $value) {
            return ['@array' => count($value), '@items' => array_map($shape, $value)];
        }
        $out = [];
        foreach ($value as $key => $item) $out[(string)$key] = $shape($item);
        ksort($out);
        return $out;
    }
    return gettype($value);
};
$request = static function (string $action, array $params, ?string $auth) use ($base): array {
    $body = json_encode($params, JSON_UNESCAPED_SLASHES);
    if (!is_string($body)) throw new RuntimeException('Request JSON encoding failed');
    $header = "Content-Type: application/json\r\n";
    if ($auth !== null) $header .= 'Access-Token: ' . $auth . "\r\n";
    $ctx = stream_context_create(['http' => ['method' => 'POST',
        'header' => $header, 'content' => $body, 'timeout' => 20,
        'ignore_errors' => true, 'follow_location' => 0]]);
    $start = microtime(true);
    $h = @fopen($base . '/index.php?s=/store/goods/' . $action,
        'rb', false, $ctx);
    if ($h === false) return ['complete' => false, 'http' => 0, 'json' => null];
    $raw = stream_get_contents($h, 1048577);
    $meta = stream_get_meta_data($h);
    $eof = feof($h);
    fclose($h);
    $headers = $meta['wrapper_data'] ?? [];
    preg_match('/^HTTP\/\S+\s+(\d{3})(?:\s|$)/', (string)($headers[0] ?? ''), $m);
    $http = isset($m[1]) ? (int)$m[1] : 0;
    $lengthOkay = true;
    foreach ($headers as $line) {
        if (preg_match('/^Content-Length:\s*(\d+)\s*$/i', (string)$line, $len)) {
            $lengthOkay = is_string($raw) && strlen($raw) === (int)$len[1];
        }
    }
    $json = is_string($raw) && strlen($raw) <= 1048576
        ? json_decode($raw, true) : null;
    $complete = $http > 0 && is_string($raw) && strlen($raw) <= 1048576
        && $eof && !($meta['timed_out'] ?? false) && $lengthOkay
        && is_array($json) && json_last_error() === JSON_ERROR_NONE;
    return ['complete' => $complete, 'http' => $http,
        'json' => $complete ? $json : null,
        'elapsed_ms' => (int)round((microtime(true) - $start) * 1000)];
};
$form = static function (string $price, string $line, string $point)
    use ($productMarker, $baseIds, $deliveryId): array {
    return [
        'goods_type' => 10, 'goods_name' => $productMarker,
        'goods_no' => $productMarker, 'categoryIds' => [$baseIds['category']],
        'imagesIds' => [$baseIds['file']], 'serviceIds' => [],
        'delivery_id' => $deliveryId, 'is_ind_delivery_type' => 0,
        'delivery_type' => [10], 'status' => 20, 'sort' => 90000,
        'spec_type' => 10, 'goods_price' => $price,
        'line_price' => $line, 'stock_num' => 0,
        'goods_weight' => 0.5, 'deduct_stock_type' => 20,
        'is_restrict' => 0, 'restrict_total' => 0,
        'restrict_single' => 0, 'content' => '<p>' . $productMarker . '</p>',
        'selling_point' => $point, 'sales_initial' => 0,
        'is_points_gift' => 0, 'is_points_discount' => 0,
        'is_enable_grade' => 0, 'is_alone_grade' => 0,
        'points_discount_config' => '',
    ];
};
$checkProduct = static function (array $p, string $price, string $line)
    use ($productMarker, $baseIds, $deliveryId): void {
    $g = $p['goods'];
    if ((string)$g['goods_name'] !== $productMarker
        || (int)$g['status'] !== 20 || (int)$g['stock_total'] !== 0
        || (int)$g['is_delete'] !== 0
        || (int)$g['delivery_id'] !== $deliveryId
        || abs((float)$g['goods_price_min'] - (float)$price) > 0.0001
        || abs((float)$g['goods_price_max'] - (float)$price) > 0.0001
        || abs((float)$p['sku']['goods_price'] - (float)$price) > 0.0001
        || abs((float)$p['sku']['line_price'] - (float)$line) > 0.0001
        || (int)$p['sku']['goods_id'] !== (int)$g['goods_id']
        || (int)$p['sku']['image_id'] !== 0
        || (int)$p['image']['goods_id'] !== (int)$g['goods_id']
        || (int)$p['image']['image_id'] !== $baseIds['file']
        || (int)$p['relation']['goods_id'] !== (int)$g['goods_id']
        || (int)$p['relation']['category_id'] !== $baseIds['category']) {
        throw new RuntimeException('API product/input/original-table mismatch');
    }
};
$issued = false;
$out = ['lane' => $lane, 'marker' => $marker,
    'routes' => [], 'token_deleted' => false,
    'no_file_or_oss_request' => true, 'journal_retained' => true];
try {
    $stage = 'token';
    if (!\think\facade\Cache::set($token, ['user' => [
            'store_user_id' => 2147480009, 'store_id' => 10001,
            'user_name' => $marker, 'real_name' => $marker,
            'is_super' => 1], 'store_id' => 10001,
            'is_login' => true], 300)
        || !\think\facade\Cache::has($token)) {
        throw new RuntimeException('Short File token unavailable');
    }
    $issued = true;
    $unauth = $request('add', ['form' => $form('12.34', '15.00', $marker)], null);
    if (!$unauth['complete'] || (int)($unauth['json']['status'] ?? 200) === 200
        || count($rows($connect(), 'SELECT GOODS_ID FROM ' . $table['goods']
            . ' WHERE STORE_ID=10001 AND GOODS_NAME=?', [$productMarker])) !== 0) {
        throw new RuntimeException('Unauthenticated add rejection not proven');
    }
    $stage = 'add';
    $r = $request('add', ['form' => $form('12.34', '15.00', $marker)], $token);
    if (!$r['complete'] || $r['http'] !== 200
        || (int)($r['json']['status'] ?? -1) !== 200) {
        throw new RuntimeException('Add HTTP result failed/uncertain');
    }
    $added = $capture($connect());
    $checkProduct($added, '12.34', '15.00');
    $journal['ids'] = ['goods' => (int)$added['goods']['goods_id'],
        'sku' => (int)$added['sku']['id'],
        'image' => (int)$added['image']['id'],
        'relation' => (int)$added['relation']['id']];
    $journal['phase'] = 'added'; $save($journal);
    $out['routes']['add'] = ['http' => 200, 'status' => 200,
        'elapsed_ms' => $r['elapsed_ms'], 'db_rows' => 4,
        'price_12_34' => true,
        'full_json_key_type_shape' => $shape($r['json'])];
    $stage = 'edit';
    $journal['phase'] = 'before_edit'; $save($journal);
    $r = $request('edit', ['goodsId' => $journal['ids']['goods'],
        'form' => $form('13.45', '17.00', $marker . 'EDIT')], $token);
    if (!$r['complete'] || $r['http'] !== 200
        || (int)($r['json']['status'] ?? -1) !== 200) {
        throw new RuntimeException('Edit HTTP result failed/uncertain');
    }
    $edited = $capture($connect());
    $checkProduct($edited, '13.45', '17.00');
    if ((int)$edited['goods']['goods_id'] !== $journal['ids']['goods']
        || (string)$edited['goods']['selling_point'] !== $marker . 'EDIT'
        || (int)$edited['sku']['id'] === $journal['ids']['sku']
        || (int)$edited['image']['id'] === $journal['ids']['image']
        || (int)$edited['relation']['id'] !== $journal['ids']['relation']) {
        throw new RuntimeException('Edit replacement child/field mismatch');
    }
    $journal['ids']['sku'] = (int)$edited['sku']['id'];
    $journal['ids']['image'] = (int)$edited['image']['id'];
    $journal['product_sha256'] = hash('sha256', serialize($edited));
    if ($baseSnapshot($connect()) !== $baseSha) {
        throw new RuntimeException('Base fixture changed during API writes');
    }
    $journal['phase'] = 'completed'; $save($journal);
    $out['routes']['edit'] = ['http' => 200, 'status' => 200,
        'elapsed_ms' => $r['elapsed_ms'], 'new_child_ids' => true,
        'price_13_45' => true,
        'full_json_key_type_shape' => $shape($r['json'])];
} catch (Throwable $e) {
    $out['failure_stage'] = $stage;
    $out['failure_class'] = get_class($e);
} finally {
    if ($issued) {
        \think\facade\Cache::delete($token);
        $out['token_deleted'] = !\think\facade\Cache::has($token);
    }
}
$out['pass'] = !isset($out['failure_class']) && $out['token_deleted']
    && count($out['routes']) === 2;
echo json_encode($out, JSON_UNESCAPED_SLASHES), "\n";
exit($out['pass'] ? 0 : 3);
