<?php

declare(strict_types=1);

// Minimal two-log fixture. No user/Store row writes. Default-deny all DML modes.
$modeRequested = (string)($argv[1] ?? '');
$laneRequested = (string)getenv('CDX_GIFT_LANE');
if (!in_array($modeRequested, ['--preflight', '--prepare-ab', '--prepare-c',
        '--cleanup-ab', '--cleanup-c'], true)
    || !in_array($laneRequested, ['A', 'B', 'C'], true)
    || ($modeRequested !== '--preflight'
        && ((string)getenv('CDX_GIFT_FIXTURE_APPROVED') !== 'ROOT_APPROVED_GIFT_FIXTURE_V1'
            || (string)getenv('CDX_GIFT_FIXTURE_SHA') !== hash_file('sha256', __FILE__)))) {
    echo json_encode(['status' => 'default_deny', 'http' => 0, 'dml' => 0]), PHP_EOL;
    exit(78);
}
$isPrepare = in_array($modeRequested, ['--prepare-ab', '--prepare-c'], true);
$isCleanup = in_array($modeRequested, ['--cleanup-ab', '--cleanup-c'], true);
if (($modeRequested === '--prepare-ab' || $modeRequested === '--cleanup-ab')
    && $laneRequested !== 'A') throw new RuntimeException('AB fixture must anchor A');
if (($modeRequested === '--prepare-c' || $modeRequested === '--cleanup-c')
    && $laneRequested !== 'C') throw new RuntimeException('C fixture must anchor C');
$giftPreflight = $laneRequested === 'C'
    ? '/tmp/zhct-giftlog-readonly-20261004/preflight_gift_log.php'
    : __DIR__ . '/preflight_gift_log.php';
$identityPreflight = $laneRequested === 'C'
    ? '/tmp/zhct-ai-read2-VgKO4CDY/read2_schema_preflight.php'
    : __DIR__ . '/../parallel-ai-next-read-safe4/read2_schema_preflight.php';
if (!is_file($identityPreflight) || hash_file('sha256', $identityPreflight)
    !== '8641ee015ad537d300de9c00f2c882e654b739d7c2194dd13ef4635ca9df1af0') {
    throw new RuntimeException('Gift fixture identity preflight drift');
}
if (!$isCleanup) {
    if (!is_file($giftPreflight) || hash_file('sha256', $giftPreflight)
        !== 'a54a040990767b0c539ef13ffa1cd8aed97b2dce5faa2d760a2c04ae195583c2') {
        throw new RuntimeException('Gift fixture source preflight drift');
    }
    $argv[1] = '--preflight';
    require $giftPreflight;
    $argv[1] = $modeRequested;
    if ($modeRequested === '--preflight') exit(0);
} else {
    putenv('CDX_READ2_TARGET=' . $laneRequested);
    putenv('CDX_READ2_SCHEMA_PREFLIGHT=1');
    require $identityPreflight;
}
$stage = 'after_identity';
set_exception_handler(static function (Throwable $error) use (&$stage): void {
    echo json_encode(['status' => 'gift_fixture_error', 'stage' => $stage,
        'error_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';
$storeUserTable = $dialect === 'dm' ? 'YDY_USER' : 'ydy_user';
$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('Expected exactly one owned row');
    $out = [];
    foreach (array_change_key_case($rows[0], CASE_LOWER) as $name => $value) {
        $out[$name] = is_resource($value) ? stream_get_contents($value) : $value;
    }
    return $out;
};
$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));
};
$controlled = $laneRequested === 'C'
    ? ['owner' => 'CDXPARMOB9411C2CB', 'foreign' => 'CDXPAY661BEE6B']
    : ['owner' => 'CDXPARMOB426CA4B0', 'foreign' => 'CDXPAY112FC189'];
$period = ['2026-09-01 00:00:00', '2026-10-01 00:00:00'];
$makeNewAi = 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]);
};
$verifyAiIdentity = static function (PDO $pdo) use ($dialect, $config): bool {
    if ($dialect === 'dm') {
        return strtoupper((string)$pdo->query('SELECT USER FROM DUAL')->fetchColumn())
                === 'AIZHCT_APP'
            && strtoupper((string)$pdo->query(
                "SELECT SYS_CONTEXT('USERENV','CURRENT_SCHEMA') FROM DUAL")->fetchColumn())
                === 'AIZHCT_APP';
    }
    $identity = (string)$pdo->query(
        "SELECT CONCAT(@@server_uuid,'|',@@hostname,'|',@@port,'|',DATABASE())")
        ->fetchColumn();
    return hash_equals(
        '210f925e093f71e26a8c8923f0f626e3be0a15ccff3d386bba21feef695c8a5b',
        hash('sha256', $identity))
        && (string)$pdo->query('SELECT DATABASE()')->fetchColumn()
            === (string)$config['POC_AI_SCHEMA'];
};
if (!$verifyAiIdentity($ai)) {
    throw new RuntimeException('Gift initial AI database identity outside approved target');
}
$ownerIds = [];
$userHashes = [];
$storeHashes = [];
foreach ($controlled as $role => $nick) {
    $user = $one($ai, 'SELECT * FROM ' . $userTable
        . ' WHERE NICK_NAME=? AND STORE_ID=10001 AND IS_DELETE=0', [$nick]);
    $id = (int)$user['user_id'];
    if ($id <= 0) throw new RuntimeException('Controlled user ID invalid');
    $ownerIds[$role] = $id;
    $userHashes[$role] = $digest($user);
    $mapping = $one($store, 'SELECT * FROM ' . $storeUserTable
        . ' WHERE AI_USER_ID=? AND IS_DELETE=0', [$id]);
    $storeHashes[$role] = $digest($mapping);
}
if ($isPrepare) {
    $stage = 'prepare_source';
    foreach ($ownerIds as $id) {
        if ($count($ai, 'SELECT COUNT(*) FROM ' . $giftTable
            . ' WHERE USER_ID=? AND CREATE_TIME>=? AND CREATE_TIME<?',
            [$id, $period[0], $period[1]]) !== 0) {
            throw new RuntimeException('Controlled user candidate month not empty');
        }
    }
    if ($count($ai, 'SELECT COUNT(*) FROM ' . $giftTable
        . " WHERE STORE_ID=10001 AND IDEMPOTENCY_KEY LIKE 'CDXGL5%'") !== 0) {
        throw new RuntimeException('Gift fixture prefix already present');
    }
    $marker = 'CDXGL5' . strtoupper(bin2hex(random_bytes(6)));
    $privateDir = sys_get_temp_dir() . '/cdx-giftlog-' . $marker;
    if (file_exists($privateDir) || !mkdir($privateDir, 0700)) {
        throw new RuntimeException('Private gift fixture directory unavailable');
    }
    chmod($privateDir, 0700);
    $journalPath = $privateDir . '/fixture.json';
    $journal = ['lane_group' => $laneRequested === 'C' ? 'C' : 'AB',
        'marker' => $marker, 'owner_ids' => $ownerIds,
        'user_hashes' => $userHashes, 'store_hashes' => $storeHashes,
        'period' => $period, 'log_ids' => [], 'log_hashes' => [],
        'http_states' => [], 'state' => 'before_insert'];
    $writeJournal = static function () use (&$journal, $privateDir, $journalPath): void {
        $temp = tempnam($privateDir, 'next-');
        if (!is_string($temp) || !chmod($temp, 0600)) {
            throw new RuntimeException('Gift fixture 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('Gift fixture journal atomic write failed');
        }
    };
    $writeJournal();
    $stage = 'insert_two_gift_logs';
    $ai->beginTransaction();
    try {
        $describeColumn = $dialect === 'dm' ? '"DESCRIBE"' : '`describe`';
        $insert = $ai->prepare('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,?)');
        $cases = [
            'owner' => [20, '-0.01', '200.00', '199.99', 'consume', 'O'],
            'foreign' => [10, '0.01', '0.00', '0.01', 'recharge', 'F'],
        ];
        foreach ($cases as $role => $case) {
            [$scene, $money, $before, $after, $type, $suffix] = $case;
            $key = $marker . ':' . $suffix;
            $insert->execute([$ownerIds[$role], $scene, $money, $before, $after,
                $type, 0, $marker . $suffix, $key,
                'Codex compatibility fixture', $marker, $marker, 'CODEX_TEST',
                '2026-09-15 12:00:00']);
            $row = $one($ai, 'SELECT * FROM ' . $giftTable
                . ' WHERE IDEMPOTENCY_KEY=? AND USER_ID=? AND STORE_ID=10001',
                [$key, $ownerIds[$role]]);
            $journal['log_ids'][$role] = (int)$row['log_id'];
            $journal['log_hashes'][$role] = $digest($row);
        }
        $ai->commit();
    } catch (Throwable $error) {
        if ($ai->inTransaction()) $ai->rollBack();
        $journal['state'] = 'requires_manual_check_after_insert_error';
        $writeJournal();
        throw $error;
    }
    $journal['state'] = 'prepared_two_logs';
    $writeJournal();
    echo json_encode(['status' => 'fixture_prepared',
        'lane_group' => $journal['lane_group'], 'journal' => $journalPath,
        'marker_sha12' => substr(hash('sha256', $marker), 0, 12),
        'gift_log_rows' => count($journal['log_ids']),
        'user_rows_written' => 0, 'store_rows_written' => 0,
        'business_http' => 0], JSON_UNESCAPED_SLASHES), PHP_EOL;
    exit(0);
}

$stage = 'cleanup_journal';
$journalPath = (string)getenv('CDX_GIFT_JOURNAL');
$privateDir = dirname($journalPath);
if (basename($journalPath) !== 'fixture.json'
    || strpos(basename($privateDir), 'cdx-giftlog-CDXGL5') !== 0
    || !is_file($journalPath) || is_link($journalPath)
    || !is_dir($privateDir) || is_link($privateDir)
    || (((int)fileperms($privateDir) & 0077) !== 0)
    || (((int)fileperms($journalPath) & 0077) !== 0)) {
    throw new RuntimeException('Gift private journal boundary invalid');
}
$journal = json_decode((string)file_get_contents($journalPath), true);
if (!is_array($journal) || ($journal['lane_group'] ?? '')
    !== ($laneRequested === 'C' ? 'C' : 'AB')
    || !preg_match('/^CDXGL5[0-9A-F]{12}$/', (string)($journal['marker'] ?? ''))
    || ($journal['period'] ?? null) !== $period
    || count($journal['log_ids'] ?? []) !== 2) {
    throw new RuntimeException('Gift private journal values invalid');
}
$marker = (string)$journal['marker'];
if (($journal['state'] ?? '') === 'http_unknown'
    || in_array('unknown', $journal['http_states'] ?? [], true)
    || in_array('inflight', $journal['http_states'] ?? [], true)) {
    throw new RuntimeException('Unknown HTTP: no concurrent fixture cleanup');
}
foreach ($journal['token_states'] ?? [] as $state) {
    if ($state !== 'zero') {
        throw new RuntimeException('Short File-cache token not independently zero');
    }
}
if (($journal['lane_group'] ?? '') === 'AB'
    && (!isset($journal['token_states']['A'], $journal['token_states']['B']))
    && (string)getenv('CDX_GIFT_ABORT_CLEANUP') !== 'ROOT_APPROVED_KNOWN_ABORT') {
    throw new RuntimeException('AB read sequence incomplete without known-abort approval');
}
foreach ($controlled as $role => $nick) {
    if ((int)($journal['owner_ids'][$role] ?? 0) !== $ownerIds[$role]
        || ($journal['user_hashes'][$role] ?? '') !== $userHashes[$role]
        || ($journal['store_hashes'][$role] ?? '') !== $storeHashes[$role]) {
        throw new RuntimeException('Gift actor row changed before cleanup');
    }
    $key = $marker . ':' . ($role === 'owner' ? 'O' : 'F');
    $row = $one($ai, 'SELECT * FROM ' . $giftTable
        . ' WHERE LOG_ID=? AND USER_ID=? AND IDEMPOTENCY_KEY=? AND STORE_ID=10001',
        [(int)$journal['log_ids'][$role], $ownerIds[$role], $key]);
    if ($digest($row) !== (string)$journal['log_hashes'][$role]) {
        throw new RuntimeException('Gift log row changed before cleanup');
    }
}
$stage = 'delete_exact_two_logs';
$ai->beginTransaction();
try {
    foreach (['owner' => 'O', 'foreign' => 'F'] as $role => $suffix) {
        $stmt = $ai->prepare('DELETE FROM ' . $giftTable
            . ' WHERE LOG_ID=? AND USER_ID=? AND IDEMPOTENCY_KEY=? AND STORE_ID=10001');
        $stmt->execute([(int)$journal['log_ids'][$role], $ownerIds[$role],
            $marker . ':' . $suffix]);
        if ($stmt->rowCount() !== 1) {
            throw new RuntimeException('Gift exact cleanup affected rows mismatch');
        }
    }
    $ai->commit();
} catch (Throwable $error) {
    if ($ai->inTransaction()) $ai->rollBack();
    throw $error;
}
$stage = 'independent_zero';
$newAi = $makeNewAi();
if (!$verifyAiIdentity($newAi)
    || $count($newAi, 'SELECT COUNT(*) FROM ' . $giftTable
        . ' WHERE IDEMPOTENCY_KEY LIKE ?', [$marker . '%']) !== 0) {
    throw new RuntimeException('Gift independent log-zero failed');
}
foreach ($controlled as $role => $nick) {
    $user = $one($newAi, 'SELECT * FROM ' . $userTable
        . ' WHERE NICK_NAME=? AND STORE_ID=10001 AND IS_DELETE=0', [$nick]);
    if ($digest($user) !== (string)$journal['user_hashes'][$role]
        || $count($newAi, 'SELECT COUNT(*) FROM ' . $giftTable
            . ' WHERE USER_ID=? AND CREATE_TIME>=? AND CREATE_TIME<?',
            [$ownerIds[$role], $period[0], $period[1]]) !== 0) {
        throw new RuntimeException('Gift independent actor or period source drift');
    }
}
if (!unlink($journalPath) || !rmdir($privateDir)) {
    throw new RuntimeException('Gift private journal cleanup incomplete');
}
echo json_encode(['status' => 'fixture_exact_zero',
    'lane_group' => $laneRequested === 'C' ? 'C' : 'AB',
    'deleted_gift_rows' => 2, 'independent_log_zero' => true,
    'user_and_store_rows_unchanged' => true,
    'journal_removed' => true, 'business_http' => 0], JSON_UNESCAPED_SLASHES), PHP_EOL;
