<?php

declare(strict_types=1);

// Gift detail only: one synthetic AI ledger row. No user/Store writes.
$mode = (string)($argv[1] ?? '');
$lane = (string)getenv('CDX_GIFT_DETAIL_LANE');
if (!in_array($mode, ['--preflight', '--prepare-ab', '--prepare-c',
        '--cleanup-ab', '--cleanup-c'], true)
    || !in_array($lane, ['A', 'B', 'C'], true)
    || ($mode !== '--preflight'
        && ((string)getenv('CDX_GIFT_DETAIL_FIXTURE_APPROVED')
                !== 'ROOT_APPROVED_GIFT_DETAIL_FIXTURE_V1'
            || (string)getenv('CDX_GIFT_DETAIL_FIXTURE_SHA')
                !== hash_file('sha256', __FILE__)))) {
    echo json_encode(['status' => 'default_deny', 'http' => 0, 'dml' => 0]), PHP_EOL;
    exit(78);
}
$prepare = in_array($mode, ['--prepare-ab', '--prepare-c'], true);
$cleanup = in_array($mode, ['--cleanup-ab', '--cleanup-c'], true);
if (($mode === '--prepare-ab' || $mode === '--cleanup-ab') && $lane !== 'A') {
    throw new RuntimeException('AB detail fixture must anchor A');
}
if (($mode === '--prepare-c' || $mode === '--cleanup-c') && $lane !== 'C') {
    throw new RuntimeException('C detail fixture must anchor C');
}
$root = $lane === 'C'
    ? '/tmp/zhct-giftlog-readonly-20261004'
    : __DIR__ . '/../parallel-ai-gift-log-http';
$preflight = $root . '/preflight_gift_log.php';
$identity = $lane === 'C'
    ? '/tmp/zhct-ai-read2-VgKO4CDY/read2_schema_preflight.php'
    : __DIR__ . '/../parallel-ai-next-read-safe4/read2_schema_preflight.php';
if (!is_file($identity) || hash_file('sha256', $identity)
    !== '8641ee015ad537d300de9c00f2c882e654b739d7c2194dd13ef4635ca9df1af0') {
    throw new RuntimeException('Gift detail identity preflight drift');
}
if (!$cleanup) {
    if (!is_file($preflight) || hash_file('sha256', $preflight)
        !== 'a54a040990767b0c539ef13ffa1cd8aed97b2dce5faa2d760a2c04ae195583c2') {
        throw new RuntimeException('Gift detail source preflight drift');
    }
    putenv('CDX_GIFT_LANE=' . $lane);
    $argv[1] = '--preflight';
    require $preflight;
    $argv[1] = $mode;
} else {
    putenv('CDX_READ2_TARGET=' . $lane);
    putenv('CDX_READ2_SCHEMA_PREFLIGHT=1');
    require $identity;
}
$stage = 'after_identity';
set_exception_handler(static function (Throwable $error) use (&$stage): void {
    echo json_encode(['status' => 'gift_detail_fixture_error',
        'stage' => $stage, 'class' => get_class($error),
        'sql_code' => (string)$error->getCode(),
        'reason_sha' => hash('sha256', $error->getMessage()),
        'business_http' => 0], JSON_UNESCAPED_SLASHES), PHP_EOL;
    exit(2);
});
$ai->setAttribute(PDO::ATTR_CASE, PDO::CASE_LOWER);
$store->setAttribute(PDO::ATTR_CASE, PDO::CASE_LOWER);
$giftTable = $dialect === 'dm' ? 'YOSHOP_USER_GIFT_LOG' : 'yoshop_user_gift_log';
$userTable = $dialect === 'dm' ? 'YOSHOP_USER' : 'yoshop_user';
$storeUser = $dialect === 'dm' ? 'YDY_USER' : 'ydy_user';
$ownerNick = $lane === 'C' ? 'CDXPARMOB9411C2CB' : 'CDXPARMOB426CA4B0';
$foreignNick = $lane === 'C' ? 'CDXPAY661BEE6B' : 'CDXPAY112FC189';
$identityOk = static function (PDO $pdo, string $side) use ($dialect): bool {
    if ($dialect === 'dm') {
        $expected = $side === 'ai' ? 'AIZHCT_APP' : 'ZHCT_APP';
        return strtoupper((string)$pdo->query('SELECT USER FROM DUAL')->fetchColumn())
                === $expected
            && strtoupper((string)$pdo->query(
                "SELECT SYS_CONTEXT('USERENV','CURRENT_SCHEMA') FROM DUAL")->fetchColumn())
                === $expected;
    }
    $sha = $side === 'ai'
        ? '210f925e093f71e26a8c8923f0f626e3be0a15ccff3d386bba21feef695c8a5b'
        : '003eb5aecf1cd4595b609c1f7f20b9ee325b578309fb9fe14fedaf77584fa401';
    $value = (string)$pdo->query(
        "SELECT CONCAT(@@server_uuid,'|',@@hostname,'|',@@port,'|',DATABASE())")
        ->fetchColumn();
    return hash_equals($sha, hash('sha256', $value));
};
if (!$identityOk($ai, 'ai') || !$identityOk($store, 'store')) {
    throw new RuntimeException('Gift detail two customer database identities drift');
}
$one = static function (PDO $pdo, string $sql, array $args = []): array {
    $stmt = $pdo->prepare($sql);
    $stmt->execute($args);
    $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
    if (count($rows) !== 1) throw new RuntimeException('Controlled row not unique');
    $result = [];
    foreach (array_change_key_case($rows[0], CASE_LOWER) as $key => $value) {
        $result[$key] = is_resource($value) ? stream_get_contents($value) : $value;
    }
    return $result;
};
$count = static function (PDO $pdo, string $sql, array $args = []): int {
    $stmt = $pdo->prepare($sql);
    $stmt->execute($args);
    return (int)$stmt->fetchColumn();
};
$digest = static function (array $row): string {
    ksort($row, SORT_STRING);
    return hash('sha256', serialize($row));
};
$owner = $one($ai, 'SELECT * FROM ' . $userTable
    . ' WHERE NICK_NAME=? AND STORE_ID=10001 AND IS_DELETE=0', [$ownerNick]);
$foreign = $one($ai, 'SELECT * FROM ' . $userTable
    . ' WHERE NICK_NAME=? AND STORE_ID=10001 AND IS_DELETE=0', [$foreignNick]);
$ownerId = (int)$owner['user_id'];
$foreignId = (int)$foreign['user_id'];
if ($ownerId <= 0 || $foreignId <= 0 || $ownerId === $foreignId) {
    throw new RuntimeException('Gift detail actor identities invalid');
}
$ownerStore = $one($store, 'SELECT * FROM ' . $storeUser
    . ' WHERE AI_USER_ID=? AND IS_DELETE=0', [$ownerId]);
$foreignStore = $one($store, 'SELECT * FROM ' . $storeUser
    . ' WHERE AI_USER_ID=? AND IS_DELETE=0', [$foreignId]);
$markerPrefix = 'CDXGD5';
$stage = 'detail_column_type';
if ($dialect === 'dm') {
    $column = $one($ai, 'SELECT DATA_TYPE FROM USER_TAB_COLUMNS'
        . ' WHERE TABLE_NAME=? AND COLUMN_NAME=?', [$giftTable, 'CREATE_TIME']);
    $dateType = strtoupper((string)$column['data_type']);
    if ($dateType !== 'DATETIME') throw new RuntimeException('DM detail create_time not DATETIME');
} else {
    $column = $one($ai, 'SELECT DATA_TYPE FROM INFORMATION_SCHEMA.COLUMNS'
        . ' WHERE TABLE_SCHEMA=? AND TABLE_NAME=? AND COLUMN_NAME=?',
        [(string)$config['POC_AI_SCHEMA'], $giftTable, 'create_time']);
    $dateType = strtoupper((string)$column['data_type']);
    if ($dateType !== 'INT') throw new RuntimeException('Dev MySQL detail create_time not INT');
}
$dateBusiness = '2026-09-15 12:00:00';
$mysqlEpoch = strtotime('2026-09-15 12:00:00 Asia/Shanghai');
if ($mysqlEpoch !== 1789444800) throw new RuntimeException('Fixed Asia/Shanghai epoch drift');
$dateBind = $dialect === 'dm' ? $dateBusiness : $mysqlEpoch;
$oldPrefix = $count($ai, 'SELECT COUNT(*) FROM ' . $giftTable
    . ' WHERE IDEMPOTENCY_KEY LIKE ?', [$markerPrefix . '%']);
if (!$cleanup && $oldPrefix !== 0) {
    throw new RuntimeException('Old gift detail marker residue');
}
if ($cleanup && $oldPrefix !== 1) {
    throw new RuntimeException('Detail cleanup prefix row count not exactly one');
}
if ($mode === '--preflight') {
    echo json_encode(['status' => 'gift_detail_fixture_preflight_pass',
        'lane' => $lane, 'date_column_type' => $dateType,
        'owner_user_sha12' => substr(hash('sha256', (string)$ownerId), 0, 12),
        'foreign_user_sha12' => substr(hash('sha256', (string)$foreignId), 0, 12),
        'owner_existing_gift_rows' => $count($ai, 'SELECT COUNT(*) FROM '
            . $giftTable . ' WHERE USER_ID=?', [$ownerId]),
        'owned_prefix_rows' => 0, 'http' => 0, 'dml' => 0],
        JSON_UNESCAPED_SLASHES), PHP_EOL;
    exit(0);
}
$newAi = static function () use ($config, $dialect): PDO {
    $dsn = $dialect === 'dm'
        ? 'dm:host=127.0.0.1;port=5238;schema=AIZHCT_APP'
        : 'mysql:host=' . $config['POC_AI_HOST'] . ';port=' . $config['POC_AI_PORT']
            . ';dbname=' . $config['POC_AI_SCHEMA'] . ';charset=utf8';
    return new PDO($dsn, $config['POC_AI_USER'], $config['POC_AI_PASSWORD'],
        [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_CASE => PDO::CASE_LOWER, PDO::ATTR_TIMEOUT => 5]);
};
$newStore = static function () use ($config, $dialect): PDO {
    $dsn = $dialect === 'dm'
        ? 'dm:host=127.0.0.1;port=5237;schema=ZHCT_APP'
        : 'mysql:host=' . $config['POC_HOST'] . ';port=' . $config['POC_PORT']
            . ';dbname=' . $config['POC_SCHEMA'] . ';charset=utf8';
    return new PDO($dsn, $config['POC_USER'], $config['POC_PASSWORD'],
        [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_CASE => PDO::CASE_LOWER, PDO::ATTR_TIMEOUT => 5]);
};
if ($prepare) {
    $stage = 'prepare_owned_source';
    if ($count($ai, 'SELECT COUNT(*) FROM ' . $giftTable . ' WHERE USER_ID=?',
        [$ownerId]) !== 0) {
        throw new RuntimeException('Detail owner has prior gift rows');
    }
    $marker = $markerPrefix . strtoupper(bin2hex(random_bytes(6)));
    $dir = sys_get_temp_dir() . '/cdx-gift-detail-' . $marker;
    if (file_exists($dir) || !mkdir($dir, 0700)) {
        throw new RuntimeException('Private detail journal directory unavailable');
    }
    chmod($dir, 0700);
    $journalPath = $dir . '/fixture.json';
    $journal = ['mode' => $lane === 'C' ? 'C' : 'AB',
        'marker' => $marker, 'owner_id' => $ownerId,
        'foreign_id' => $foreignId,
        'owner_user_sha' => $digest($owner),
        'foreign_user_sha' => $digest($foreign),
        'owner_store_sha' => $digest($ownerStore),
        'foreign_store_sha' => $digest($foreignStore),
        'create_time_business' => $dateBusiness,
        'create_time_column_type' => $dateType,
        'log_id' => 0, 'log_sha' => '',
        'http_states' => [], 'token_states' => [],
        'state' => 'before_insert'];
    $writeJournal = static function () use (&$journal, $dir, $journalPath): void {
        $temp = tempnam($dir, 'next-');
        if (!is_string($temp) || !chmod($temp, 0600)) {
            throw new RuntimeException('Private detail journal temp unavailable');
        }
        $json = json_encode($journal, JSON_UNESCAPED_SLASHES);
        if (!is_string($json) || file_put_contents($temp, $json, LOCK_EX) !== strlen($json)
            || !rename($temp, $journalPath)) {
            throw new RuntimeException('Private detail journal write failed');
        }
    };
    $writeJournal();
    $stage = 'insert_one_owned_gift_log';
    $ai->beginTransaction();
    try {
        $describeColumn = $dialect === 'dm' ? '"DESCRIBE"' : '`describe`';
        $sql = 'INSERT INTO ' . $giftTable
            . ' (USER_ID,SCENE,MONEY,BALANCE_BEFORE,BALANCE_AFTER,BIZ_TYPE,'
            . ' BIZ_ORDER_ID,BIZ_ORDER_NO,IDEMPOTENCY_KEY,' . $describeColumn . ',REMARK,'
            . ' OPERATOR_ID,OPERATOR_NAME,STORE_ID,CREATE_TIME)'
            . ' VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,10001,?)';
        $ai->prepare($sql)->execute([$ownerId, 20, '-0.01', '200.00', '199.99',
            'consume', 0, $marker . 'O', $marker . ':O',
            'Codex detail compatibility fixture', $marker, $marker,
            'CODEX_TEST', $dateBind]);
        $log = $one($ai, 'SELECT * FROM ' . $giftTable
            . ' WHERE USER_ID=? AND IDEMPOTENCY_KEY=? AND STORE_ID=10001',
            [$ownerId, $marker . ':O']);
        $storedDate = (string)$log['create_time'];
        if ((int)$log['log_id'] <= 0
            || ($dialect === 'dm'
                ? substr($storedDate, 0, 19) !== $dateBusiness
                : $storedDate !== (string)$mysqlEpoch)) {
            throw new RuntimeException('Detail actual date column write/read mismatch');
        }
        $journal['log_id'] = (int)$log['log_id'];
        $journal['log_sha'] = $digest($log);
        $ai->commit();
    } catch (Throwable $error) {
        if ($ai->inTransaction()) $ai->rollBack();
        $journal['state'] = 'requires_manual_check_after_insert_error';
        $writeJournal();
        throw $error;
    }
    $journal['state'] = 'prepared_one_detail_log';
    $writeJournal();
    echo json_encode(['status' => 'detail_fixture_prepared',
        'lane_group' => $journal['mode'],
        'marker_sha12' => substr(hash('sha256', $marker), 0, 12),
        'journal' => $journalPath, 'gift_log_rows' => 1,
        'user_rows_written' => 0, 'store_rows_written' => 0,
        'business_http' => 0], JSON_UNESCAPED_SLASHES), PHP_EOL;
    exit(0);
}
$stage = 'cleanup_private_journal';
$journalPath = (string)getenv('CDX_GIFT_DETAIL_JOURNAL');
$dir = dirname($journalPath);
if (basename($journalPath) !== 'fixture.json'
    || strpos(basename($dir), 'cdx-gift-detail-CDXGD5') !== 0
    || !is_dir($dir) || is_link($dir)
    || !is_file($journalPath) || is_link($journalPath)
    || (((int)fileperms($dir) & 0077) !== 0)
    || (((int)fileperms($journalPath) & 0077) !== 0)) {
    throw new RuntimeException('Private detail cleanup journal invalid');
}
$journal = json_decode((string)file_get_contents($journalPath), true);
if (!is_array($journal) || !preg_match('/^CDXGD5[0-9A-F]{12}$/',
        (string)($journal['marker'] ?? ''))
    || ($journal['mode'] ?? '') !== ($lane === 'C' ? 'C' : 'AB')
    || (int)($journal['owner_id'] ?? 0) !== $ownerId
    || (int)($journal['foreign_id'] ?? 0) !== $foreignId
    || (int)($journal['log_id'] ?? 0) <= 0
    || ($journal['create_time_business'] ?? '') !== $dateBusiness
    || ($journal['create_time_column_type'] ?? '') !== $dateType) {
    throw new RuntimeException('Detail journal ownership/date drift');
}
foreach ($journal['http_states'] ?? [] as $state) {
    if ($state === 'unknown' || $state === 'inflight') {
        throw new RuntimeException('Unknown detail HTTP, no concurrent delete');
    }
}
foreach ($journal['token_states'] ?? [] as $state) {
    if ($state !== 'zero') throw new RuntimeException('Detail token not independently zero');
}
if ($lane === 'A'
    && (!isset($journal['token_states']['A'], $journal['token_states']['B']))
    && (string)getenv('CDX_GIFT_DETAIL_ABORT_CLEANUP')
        !== 'ROOT_APPROVED_KNOWN_DETAIL_ABORT') {
    throw new RuntimeException('AB detail reads incomplete without abort approval');
}
if ($digest($owner) !== (string)$journal['owner_user_sha']
    || $digest($foreign) !== (string)$journal['foreign_user_sha']
    || $digest($ownerStore) !== (string)$journal['owner_store_sha']
    || $digest($foreignStore) !== (string)$journal['foreign_store_sha']) {
    throw new RuntimeException('Detail old synthetic user or Store mapping changed');
}
$marker = (string)$journal['marker'];
$log = $one($ai, 'SELECT * FROM ' . $giftTable
    . ' WHERE LOG_ID=? AND USER_ID=? AND IDEMPOTENCY_KEY=? AND STORE_ID=10001',
    [(int)$journal['log_id'], $ownerId, $marker . ':O']);
if ($digest($log) !== (string)$journal['log_sha']) {
    throw new RuntimeException('Detail one owned log changed before cleanup');
}
$stage = 'delete_one_exact_detail_log';
$ai->beginTransaction();
try {
    $stmt = $ai->prepare('DELETE FROM ' . $giftTable
        . ' WHERE LOG_ID=? AND USER_ID=? AND IDEMPOTENCY_KEY=? AND STORE_ID=10001');
    $stmt->execute([(int)$journal['log_id'], $ownerId, $marker . ':O']);
    if ($stmt->rowCount() !== 1) {
        throw new RuntimeException('Detail exact cleanup affected rows mismatch');
    }
    $ai->commit();
} catch (Throwable $error) {
    if ($ai->inTransaction()) $ai->rollBack();
    throw $error;
}
$stage = 'independent_detail_zero';
$check = $newAi();
$checkStore = $newStore();
if (!$identityOk($check, 'ai') || !$identityOk($checkStore, 'store')
    || $count($check, 'SELECT COUNT(*) FROM ' . $giftTable
        . ' WHERE IDEMPOTENCY_KEY LIKE ?', [$marker . '%']) !== 0
    || $count($check, 'SELECT COUNT(*) FROM ' . $giftTable
        . ' WHERE USER_ID=?', [$ownerId]) !== 0) {
    throw new RuntimeException('Detail independent new-PDO log-zero failed');
}
$ownerAfter = $one($check, 'SELECT * FROM ' . $userTable
    . ' WHERE USER_ID=? AND NICK_NAME=? AND STORE_ID=10001 AND IS_DELETE=0',
    [$ownerId, $ownerNick]);
$foreignAfter = $one($check, 'SELECT * FROM ' . $userTable
    . ' WHERE USER_ID=? AND NICK_NAME=? AND STORE_ID=10001 AND IS_DELETE=0',
    [$foreignId, $foreignNick]);
$ownerStoreAfter = $one($checkStore, 'SELECT * FROM ' . $storeUser
    . ' WHERE AI_USER_ID=? AND IS_DELETE=0', [$ownerId]);
$foreignStoreAfter = $one($checkStore, 'SELECT * FROM ' . $storeUser
    . ' WHERE AI_USER_ID=? AND IS_DELETE=0', [$foreignId]);
if ($digest($ownerAfter) !== (string)$journal['owner_user_sha']
    || $digest($foreignAfter) !== (string)$journal['foreign_user_sha']
    || $digest($ownerStoreAfter) !== (string)$journal['owner_store_sha']
    || $digest($foreignStoreAfter) !== (string)$journal['foreign_store_sha']) {
    throw new RuntimeException('Detail independent owner/foreign/Store row changed');
}
if (!unlink($journalPath) || !rmdir($dir)) {
    throw new RuntimeException('Private detail journal not exactly removed');
}
echo json_encode(['status' => 'detail_fixture_exact_zero',
    'lane_group' => $lane === 'C' ? 'C' : 'AB',
    'deleted_gift_rows' => 1, 'independent_log_zero' => true,
    'old_user_and_Store_rows_unchanged' => true,
    'journal_removed' => true, 'business_http' => 0],
    JSON_UNESCAPED_SLASHES), PHP_EOL;
