<?php
declare(strict_types=1);

// Redact even failures that happen before the main try/finally is installed.
$GLOBALS['cdx_stage'] = 'bootstrap';
set_exception_handler(static function (Throwable $error): void {
    fwrite(STDERR, json_encode([
        'status' => 'fatal-redacted',
        'stage' => $GLOBALS['cdx_stage'] ?? 'bootstrap',
        'class' => get_class($error),
        'code' => (string)$error->getCode(),
    ], JSON_UNESCAPED_SLASHES) . PHP_EOL);
    exit(2);
});
set_error_handler(static function (int $severity): bool {
    if (!(error_reporting() & $severity)) return false;
    throw new ErrorException('redacted warning', 0, $severity);
});

// Procedural execution gate, not a credential. Root approval is still required.
const CDX_EXECUTION_TOKEN = 'ZHCT_ODBC_SYNTHETIC_TABLE_20261002_APPROVED_V1';
if (!hash_equals(CDX_EXECUTION_TOKEN,
    (string)getenv('CDX_SYNTH_TABLE_RUN_TOKEN'))) {
    fwrite(STDERR, "RUN_TOKEN_REQUIRED: no database connection or mutation is enabled\n");
    exit(78);
}

$GLOBALS['cdx_stage'] = 'dependency_hash_gate';
$runtime = '/tmp/zhct-ai-http-20260929.3XDj3w';
$module = (string)getenv('CDX_ODBC_MODULE_PATH');
$expectedFiles = [
    __DIR__ . '/../parallel-odbc-app-connector/PrivateOdbcDm.php'
        => 'b9b0a3d3fd342f0b7efec20b00d3961b556c67ee56affac1359b6cc24cf04b05',
    __DIR__ . '/../parallel-odbc-typed-statement/TypedStatement.php'
        => 'c012f50e66f98587676dbcbb84bf08e4f6c4753f675e0cc6fec62aa9b021b97c',
    __DIR__ . '/../../../work_store/apifox/2026-09-30-dm8-interface-acceptance/'
        . 'parallel-odbc-typed-fetch/TypedFetchNormalizer.php'
        => 'ae240ec2a1b4e07f2e9f89ff8347dbdb5beca806e7c4136ab4116d928e495035',
    $runtime . '/app/common/database/Dm.php'
        => 'ab5d44a9915af0f0a01186b0c72fc759075ebe9651a6c28cee40123a65218727',
    $runtime . '/app/common/database/DmBuilder.php'
        => '3f9d65cce7f8128ee4ed0395125b48f480a19871d23ea05f50d94906d73bcb81',
    $module
        => 'cbc8889f144b53890059b0878fa8eb1e2e362f0e45b7f2f3aeb98cf8b9c8b4f7',
];
if ($module === '' || !is_file($runtime . '/vendor/autoload.php')) {
    throw new RuntimeException('Approved runtime dependency unavailable');
}
foreach ($expectedFiles as $path => $sha256) {
    if (!is_file($path) || hash_file('sha256', $path) !== $sha256) {
        throw new RuntimeException('Approved runtime hash mismatch');
    }
}
require_once $runtime . '/vendor/autoload.php';
require_once __DIR__ . '/../parallel-odbc-app-connector/PrivateOdbcDm.php';

$private = (string)getenv('CDX_ODBC_PRIVATE_DIR');
$runDir = (string)getenv('CDX_ODBC_RUN_DIR');
$pid = (int)getenv('CDX_AI_POC_PID');
if (!preg_match('~^/tmp/zhct-pdo-odbc-[A-Za-z0-9]{8}$~D', $private)
    || !is_dir($private . '/odbc-config')
    || !preg_match('~^/tmp/zhct-odbc-table-[A-Za-z0-9]{8}$~D', $runDir)
    || !is_dir($runDir) || $pid <= 1) {
    throw new RuntimeException('Private test boundary unavailable');
}
if ((fileperms($runDir) & 0777) !== 0700
    || (fileperms($private . '/odbc-config') & 0777) !== 0700
    || (fileperms($private . '/odbc-config/odbc.ini') & 0777) !== 0600
    || (fileperms($private . '/odbc-config/odbcinst.ini') & 0777) !== 0600) {
    throw new RuntimeException('Private test permissions unavailable');
}
$ini = parse_ini_file($private . '/odbc-config/odbc.ini', true, INI_SCANNER_RAW);
$drivers = parse_ini_file($private . '/odbc-config/odbcinst.ini', true, INI_SCANNER_RAW);
$source = is_array($ini) ? ($ini['CDX_PRIVATE_AI'] ?? []) : [];
$driverName = $source['Driver'] ?? null;
$driverFile = is_array($drivers) && is_string($driverName)
    ? ($drivers[$driverName]['Driver'] ?? null) : null;
if (($source['SERVER'] ?? '') !== '127.0.0.1'
    || (string)($source['TCP_PORT'] ?? '') !== '5238'
    || !is_string($driverFile) || !is_file($driverFile)
    || isset($source['UID']) || isset($source['PWD']) || isset($source['PASSWORD'])) {
    throw new RuntimeException('Private non-production AI DSN mismatch');
}
$raw = file_get_contents('/proc/' . $pid . '/environ');
if ($raw === false) throw new RuntimeException('Approved POC process unavailable');
$env = [];
foreach (explode("\0", $raw) as $part) {
    if (strpos($part, 'POC_') !== 0) continue;
    $position = strpos($part, '=');
    if ($position !== false) $env[substr($part, 0, $position)] = substr($part, $position + 1);
}
if (($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'
    || empty($env['POC_AI_USER']) || empty($env['POC_AI_PASSWORD'])) {
    throw new RuntimeException('Process is not the approved isolated AI DM POC');
}
$cmdline = file_get_contents('/proc/' . $pid . '/cmdline');
if ($cmdline === false
    || strpos($cmdline, '/tmp/zhct-ai-http-20260929.3XDj3w/public') === false
    || strpos($cmdline, '127.0.0.1:18102') === false) {
    throw new RuntimeException('Process is not the isolated AI HTTP runtime');
}
putenv('ODBCSYSINI=' . $private . '/odbc-config');
putenv('ODBCINI=' . $private . '/odbc-config/odbc.ini');
putenv('ODBCINSTINI=odbcinst.ini');

$table = 'CDXODBC_' . gmdate('Ymd') . '_' . strtoupper(bin2hex(random_bytes(4)));
if (!preg_match('/^CDXODBC_[0-9]{8}_[A-F0-9]{8}$/D', $table)) {
    throw new RuntimeException('Generated table name invalid');
}
$quoted = '"' . $table . '"';
$marker = $runDir . '/table-name.txt';
$markerHandle = fopen($marker, 'x');
if ($markerHandle === false) {
    throw new RuntimeException('Unable to record exact cleanup target');
}
$markerText = $table . PHP_EOL;
if (fwrite($markerHandle, $markerText) !== strlen($markerText)) {
    fclose($markerHandle);
    throw new RuntimeException('Unable to write exact cleanup target');
}
fclose($markerHandle);
chmod($marker, 0600);

$stage = 'connect';
$created = false;
$connection = null;
$pdo = null;
$checks = [];
$error = null;
$cleanup = 'not-required';
$rowTypes = [];
$aggregate = [];
$aggregateRawPdo = null;
$tableCount = static function (PDO $pdo, string $name): int {
    $stmt = $pdo->prepare('SELECT COUNT(*) FROM USER_TABLES WHERE TABLE_NAME=?');
    $stmt->execute([$name]);
    return (int)$stmt->fetchColumn();
};
$ensure = static function (bool $condition, string $name) use (&$checks): void {
    $checks[$name] = $condition;
    if (!$condition) throw new RuntimeException('Synthetic test check failed: ' . $name);
};

try {
    $db = new \think\DbManager();
    $db->setConfig([
        'default' => 'private_ai',
        'connections' => [
            'private_ai' => [
                'type' => '\\' . PrivateOdbcDm::class,
                'hostname' => '127.0.0.1',
                'hostport' => 5238,
                'database' => 'AIZHCT_APP',
                'username' => $env['POC_AI_USER'],
                'password' => $env['POC_AI_PASSWORD'],
                'trigger_sql' => false,
            ],
        ],
    ]);
    $connection = $db->connect();
    $pdo = $connection->connect();
    $stage = 'identity_and_permission';
    $ensure(strtoupper((string)$pdo->query('SELECT USER FROM DUAL')->fetchColumn())
        === 'AIZHCT_APP', 'expected_login_user');
    $ensure(strtoupper((string)$pdo->query(
        "SELECT SYS_CONTEXT('USERENV','CURRENT_SCHEMA') FROM DUAL"
    )->fetchColumn()) === 'AIZHCT_APP', 'expected_current_schema');
    $privilege = $pdo->query(
        "SELECT COUNT(*) FROM USER_SYS_PRIVS WHERE PRIVILEGE='CREATE TABLE'"
    )->fetchColumn();
    $ensure((int)$privilege > 0, 'direct_create_table_privilege');
    $ensure($tableCount($pdo, $table) === 0, 'new_table_absent');

    $stage = 'create_exact_table';
    echo json_encode(['stage' => $stage, 'table' => $table], JSON_UNESCAPED_SLASHES), PHP_EOL;
    $pdo->exec('CREATE TABLE ' . $quoted . ' ('
        . 'PK_ID INT PRIMARY KEY, V_DEC30 DECIMAL(30,2), '
        . 'V_DEC20 DECIMAL(20,0), V_INT INT, V_DOUBLE DOUBLE, '
        . 'V_TIME DATETIME, V_NULL VARCHAR(16))');
    $created = true;
    $ensure($tableCount($pdo, $table) === 1, 'created_exact_table_visible');
    $columnStmt = $pdo->prepare(
        'SELECT COLUMN_NAME, DATA_TYPE FROM USER_TAB_COLUMNS '
        . 'WHERE TABLE_NAME=? ORDER BY COLUMN_ID');
    $columnStmt->execute([$table]);
    $columns = $columnStmt->fetchAll(PDO::FETCH_ASSOC);
    $ensure(count($columns) === 7, 'seven_expected_columns');

    $stage = 'bound_insert_and_commit';
    $connection->startTrans();
    $insert = 'INSERT INTO ' . $quoted
        . ' (PK_ID,V_DEC30,V_DEC20,V_INT,V_DOUBLE,V_TIME,V_NULL)'
        . ' VALUES (?,?,?,?,?,?,?)';
    $connection->getPDOStatement($insert, [
        [1, PDO::PARAM_INT], ['9007199254740993.01', PDO::PARAM_STR],
        ['9007199254740993', PDO::PARAM_STR], [7, PDO::PARAM_INT],
        ['1.25', PDO::PARAM_STR], ['2026-10-02 12:34:56', PDO::PARAM_STR],
        [null, PDO::PARAM_NULL],
    ]);
    $connection->getPDOStatement($insert, [
        [2, PDO::PARAM_INT], ['-1234567890123456.70', PDO::PARAM_STR],
        ['18446744073709551615', PDO::PARAM_STR], [-42, PDO::PARAM_INT],
        ['-3.5', PDO::PARAM_STR], ['2024-02-29 23:59:59', PDO::PARAM_STR],
        ['SYNTHETIC', PDO::PARAM_STR],
    ]);
    $connection->commit();

    $stage = 'raw_table_readback';
    $rawRows = $connection->query('SELECT PK_ID,V_DEC30,V_DEC20,V_INT,'
        . 'V_DOUBLE,V_TIME,V_NULL FROM ' . $quoted . ' ORDER BY PK_ID');
    $ensure(count($rawRows) === 2, 'two_committed_rows');
    $ensure(($rawRows[0]['v_dec30'] ?? null) === '9007199254740993.01',
        'decimal30_exact');
    $ensure(($rawRows[0]['v_dec20'] ?? null) === '9007199254740993',
        'decimal20_exact_string');
    $ensure(($rawRows[0]['v_int'] ?? null) === 7, 'int_php_type');
    $ensure(($rawRows[0]['v_double'] ?? null) === 1.25, 'double_php_type');
    $ensure(array_key_exists('v_null', $rawRows[0])
        && $rawRows[0]['v_null'] === null, 'sql_null_preserved');
    $ensure(($rawRows[1]['v_dec20'] ?? null) === '18446744073709551615',
        'uint64_text_exact');
    $ensure(($rawRows[1]['v_dec30'] ?? null) === '-1234567890123456.70',
        'negative_decimal30_exact');
    $ensure(($rawRows[1]['v_int'] ?? null) === -42, 'negative_int_php_type');
    $ensure(($rawRows[1]['v_double'] ?? null) === -3.5, 'negative_double_php_type');
    $ensure(($rawRows[1]['v_null'] ?? null) === 'SYNTHETIC',
        'non_null_text_preserved');
    $ensure(is_string($rawRows[0]['v_time'] ?? null)
        && strpos($rawRows[0]['v_time'], '2026-10-02 12:34:56') === 0,
        'datetime_second_prefix_preserved');
    foreach ($rawRows[0] as $key => $value) $rowTypes[$key] = gettype($value);

    $stage = 'aggregate_precision_readback';
    $aggregateSql = 'SELECT SUM(V_DEC30) AS SUM_RAW, '
        . 'CAST(SUM(V_DEC30) AS DECIMAL(30,2)) AS SUM_CAST FROM ' . $quoted;
    // Raw PDO_ODBC is deliberately separate from TypedStatement: it exposes
    // the driver's original PHP type/value before any prototype conversion.
    $aggregateRawPdo = new PDO('odbc:CDX_PRIVATE_AI',
        $env['POC_AI_USER'], $env['POC_AI_PASSWORD'], [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_CASE => PDO::CASE_LOWER,
        ]);
    $aggregateStmt = $aggregateRawPdo->query($aggregateSql);
    $meta = [];
    for ($index = 0; $index < 2; ++$index) {
        $column = $aggregateStmt->getColumnMeta($index);
        $meta[] = is_array($column) ? [
            'sql_type' => $column['odbc:sql_type'] ?? null,
            'column_size' => $column['odbc:column_size'] ?? null,
            'precision' => $column['precision'] ?? null,
            'scale' => $column['scale'] ?? null,
        ] : null;
    }
    $aggregateRaw = $aggregateStmt->fetch(PDO::FETCH_ASSOC);
    $aggregateStmt->closeCursor();
    $aggregateRawPdo = null;
    $ensure(is_array($aggregateRaw), 'aggregate_raw_row_available');
    $aggregateTyped = $connection->getPDOStatement($aggregateSql)->fetch(PDO::FETCH_ASSOC);
    $ensure(is_array($aggregateTyped), 'aggregate_typed_row_available');
    $expectedSum = '7772631364617536.31';
    $rawSum = $aggregateRaw['sum_raw'] ?? null;
    $rawCast = $aggregateRaw['sum_cast'] ?? null;
    $typedSum = $aggregateTyped['sum_raw'] ?? null;
    $typedCast = $aggregateTyped['sum_cast'] ?? null;
    $aggregate = [
        'expected' => $expectedSum,
        'metadata' => $meta,
        'raw_php_types' => [
            'sum_raw' => gettype($rawSum), 'sum_cast' => gettype($rawCast),
        ],
        'raw_values' => ['sum_raw' => $rawSum, 'sum_cast' => $rawCast],
        'typed_php_types' => [
            'sum_raw' => gettype($typedSum), 'sum_cast' => gettype($typedCast),
        ],
        'typed_values' => ['sum_raw' => $typedSum, 'sum_cast' => $typedCast],
    ];
    // Record P0 evidence without hiding the other five ORM path results.
    $checks['sum_uncast_exact_non_double'] = is_string($rawSum)
        && $rawSum === $expectedSum
        && ($meta[0]['sql_type'] ?? null) !== 8;
    $checks['sum_cast_exact'] = is_string($rawCast)
        && $rawCast === $expectedSum;
    $aggregate['p0_uncast'] = !$checks['sum_uncast_exact_non_double'];

    $stage = 'transaction_rollback';
    $connection->startTrans();
    $connection->getPDOStatement(
        'UPDATE ' . $quoted . ' SET V_INT=? WHERE PK_ID=?',
        [[99, PDO::PARAM_INT], [1, PDO::PARAM_INT]]
    );
    $inside = $connection->getPDOStatement(
        'SELECT V_INT FROM ' . $quoted . ' WHERE PK_ID=?',
        [[1, PDO::PARAM_INT]]
    )->fetchColumn();
    $ensure($inside === 99, 'transaction_sees_own_update');
    $connection->rollback();
    $afterRollback = $connection->getPDOStatement(
        'SELECT V_INT FROM ' . $quoted . ' WHERE PK_ID=?',
        [[1, PDO::PARAM_INT]]
    )->fetchColumn();
    $ensure($afterRollback === 7, 'rollback_restores_original');

    $stage = 'orm_paths';
    $queryRows = $connection->query(
        'SELECT PK_ID,V_DEC30,V_DEC20,V_INT,V_DOUBLE,V_TIME,V_NULL '
        . 'FROM ' . $quoted . ' ORDER BY PK_ID');
    $found = $connection->table($table)->where('PK_ID', 1)->find();
    $value = $connection->table($table)->where('PK_ID', 1)->value('V_INT');
    $column = $connection->table($table)->order('PK_ID')->column('V_DEC20');
    $cursorRows = iterator_to_array(
        $connection->table($table)->order('PK_ID')->cursor(), false);
    $ensure(count($queryRows) === 2 && count($cursorRows) === 2,
        'query_and_cursor_two_rows');
    $ensure(($cursorRows[0]['v_dec30'] ?? null) === '9007199254740993.01'
        && ($queryRows[1]['v_dec20'] ?? null) === '18446744073709551615',
        'query_cursor_exact_high_precision');
    $ensure(($found['v_dec30'] ?? null) === '9007199254740993.01',
        'find_decimal30_exact');
    $ensure(!array_key_exists('numrow', $found), 'dm_builder_no_numrow');
    $ensure($value === 7, 'value_integer');
    $ensure($column === ['9007199254740993', '18446744073709551615'],
        'column_decimal20_exact_strings');

    $stage = 'pagination';
    $pageOne = $connection->table($table)->order('PK_ID')->page(1, 1)->select()->toArray();
    $pageTwo = $connection->table($table)->order('PK_ID')->page(2, 1)->select()->toArray();
    $ensure(count($pageOne) === 1 && ($pageOne[0]['pk_id'] ?? null) === 1,
        'page_one_id_one');
    $ensure(count($pageTwo) === 1 && ($pageTwo[0]['pk_id'] ?? null) === 2,
        'page_two_id_two');
    $ensure(($pageOne[0]['v_dec30'] ?? null) === '9007199254740993.01'
        && ($pageTwo[0]['v_dec20'] ?? null) === '18446744073709551615',
        'pagination_decimal_strings_exact');
} catch (Throwable $failure) {
    // Never print native messages: they may contain DSNs or credentials.
    $error = ['stage' => $stage, 'class' => get_class($failure),
        'code' => (string)$failure->getCode()];
} finally {
    $aggregateRawPdo = null;
    if ($created) $cleanup = 'pending_exact_table';
    if ($created && $pdo instanceof PDO) {
        try {
            if ($pdo->inTransaction()) $pdo->rollBack();
            $owner = strtoupper((string)$pdo->query('SELECT USER FROM DUAL')->fetchColumn());
            $currentSchema = strtoupper((string)$pdo->query(
                "SELECT SYS_CONTEXT('USERENV','CURRENT_SCHEMA') FROM DUAL"
            )->fetchColumn());
            if ($owner === 'AIZHCT_APP'
                && $currentSchema === 'AIZHCT_APP'
                && preg_match('/^CDXODBC_[0-9]{8}_[A-F0-9]{8}$/D', $table)
                && $tableCount($pdo, $table) === 1) {
                $pdo->exec('DROP TABLE ' . $quoted);
            }
            $cleanup = $tableCount($pdo, $table) === 0
                ? 'verified_absent' : 'exact_table_still_present';
        } catch (Throwable $cleanupError) {
            $cleanup = 'failed_or_unknown';
        }
    }
}

$pass = $error === null && $cleanup === 'verified_absent'
    && !in_array(false, $checks, true);
echo json_encode([
    'status' => $pass ? 'synthetic_table_pass' : 'synthetic_table_not_closed',
    'table' => $table, 'checks' => $checks, 'raw_row_types' => $rowTypes,
    'aggregate' => $aggregate, 'error' => $error, 'cleanup' => $cleanup,
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), PHP_EOL;
exit($pass ? 0 : 3);
