<?php
declare(strict_types=1);

// Read-only schema/identity probe for User/info and User/assets.
// It never fetches a person, amount, token, or business row.
$stage = 'bootstrap';
set_exception_handler(static function (Throwable $error) use (&$stage): void {
    echo json_encode([
        'status' => 'redacted_error',
        'stage' => $stage,
        'error_class' => get_class($error),
        'sql_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);
});

$target = (string)getenv('CDX_READ2_TARGET');
if (!in_array($target, ['A', 'B', 'C'], true)
    || getenv('CDX_READ2_SCHEMA_PREFLIGHT') !== '1') {
    echo json_encode(['status' => 'default_deny',
        'database_connected' => false, 'business_http_sent' => false,
        'dml_sent' => false], JSON_UNESCAPED_SLASHES), PHP_EOL;
    exit(78);
}
$stage = 'approved_runtime_identity';
$port = ['A' => 18105, 'B' => 18101, 'C' => 18102][$target];
$config = [];
if ($target === 'C') {
    $pid = 53772;
    $cmdline = file_get_contents('/proc/' . $pid . '/cmdline');
    $raw = file_get_contents('/proc/' . $pid . '/environ');
    if (!is_string($cmdline) || !is_string($raw)
        || strpos($cmdline, '127.0.0.1:' . $port) === false
        || strpos($cmdline, '/tmp/zhct-ai-http-20260929.3XDj3w/public') === false
        || strpos($raw, 'PHP_CLI_SERVER_WORKERS=') !== false) {
        throw new RuntimeException('C isolated runtime mismatch');
    }
    foreach (explode("\0", $raw) as $entry) {
        if (strpos($entry, 'POC_') !== 0) continue;
        $equal = strpos($entry, '=');
        if ($equal !== false) {
            $config[substr($entry, 0, $equal)] = substr($entry, $equal + 1);
        }
    }
    $expected = [
        'POC_DIALECT' => 'dm', 'POC_EXTERNAL_STUBS' => '1',
        'POC_AI_HOST' => '127.0.0.1', 'POC_AI_PORT' => '5238',
        'POC_AI_SCHEMA' => 'AIZHCT_APP',
        'POC_HOST' => '127.0.0.1', 'POC_PORT' => '5237',
        'POC_SCHEMA' => 'ZHCT_APP',
    ];
    foreach ($expected as $key => $value) {
        if (($config[$key] ?? null) !== $value) {
            throw new RuntimeException('C database boundary mismatch');
        }
    }
} else {
    $root = $target === 'A'
        ? '/private/tmp/zhct-http-ai-macos.zjaU8h'
        : '/private/tmp/zhct-http-ai-macos.x2CCZX';
    $pids = array_values(array_filter(explode("\n", trim((string)shell_exec(
        '/usr/sbin/lsof -t -nP -iTCP:' . $port . ' -sTCP:LISTEN 2>/dev/null'))),
        static function (string $item): bool { return ctype_digit($item); }));
    if (count($pids) !== 1) throw new RuntimeException('A/B listener mismatch');
    $cmdline = shell_exec('/bin/ps eww -p ' . $pids[0] . ' -o command=');
    if (!is_string($cmdline) || strpos($cmdline, '-S 127.0.0.1:' . $port) === false
        || (strpos($cmdline, $root . '/public') === false
            && strpos($cmdline, str_replace('/private/tmp/', '/tmp/', $root) . '/public') === false)
        || strpos($cmdline, 'PHP_CLI_SERVER_WORKERS=') !== false) {
        throw new RuntimeException('A/B isolated runtime mismatch');
    }
    $local = realpath('/Users/wiscourper/Documents/workspace/zhct/zhctproject/ai_api/.env.local');
    if ($local !== '/Users/wiscourper/Documents/workspace/zhct/zhctproject/ai_api/.env.local') {
        throw new RuntimeException('Local development config missing');
    }
    $ini = parse_ini_file($local, true, INI_SCANNER_RAW);
    if (!is_array($ini) || !isset($ini['DATABASE'], $ini['DATABASE_ZHCT'])) {
        throw new RuntimeException('Local database sections missing');
    }
    foreach (['DATABASE' => 'POC_AI_', 'DATABASE_ZHCT' => 'POC_'] as $section => $prefix) {
        foreach (['HOSTNAME' => 'HOST', 'HOSTPORT' => 'PORT',
            'DATABASE' => 'SCHEMA', 'USERNAME' => 'USER',
            'PASSWORD' => 'PASSWORD'] as $field => $suffix) {
            $key = $prefix . $suffix;
            $value = (string)($ini[$section][$field] ?? '');
            if ($value === '' || preg_match('/(?:^|\s)' . preg_quote($key . '=' . $value, '/')
                    . '(?=\s|$)/', $cmdline) !== 1) {
                throw new RuntimeException('A/B runtime does not match local database config');
            }
            $config[$key] = $value;
        }
    }
}

$stage = 'external_stub_boundary';
$boundary = json_decode((string)file_get_contents(
    'http://127.0.0.1:' . $port . '/__poc_boundary'), true);
if (!is_array($boundary) || ($boundary['external_stubs'] ?? null) !== true) {
    throw new RuntimeException('External stubs unavailable');
}

$stage = 'database_connect';
foreach (['POC_AI_HOST', 'POC_AI_PORT', 'POC_AI_SCHEMA', 'POC_AI_USER', 'POC_AI_PASSWORD',
    'POC_HOST', 'POC_PORT', 'POC_SCHEMA', 'POC_USER', 'POC_PASSWORD'] as $key) {
    if (empty($config[$key])) throw new RuntimeException('Approved database setting unavailable');
}
$dialect = $target === 'C' ? 'dm' : 'mysql';
$make = static function (string $prefix) use ($dialect, $config): PDO {
    $dsn = $dialect === 'dm'
        ? 'dm:host=' . $config[$prefix . 'HOST'] . ';port=' . $config[$prefix . 'PORT']
            . ';schema=' . $config[$prefix . 'SCHEMA']
        : 'mysql:host=' . $config[$prefix . 'HOST'] . ';port=' . $config[$prefix . 'PORT']
            . ';dbname=' . $config[$prefix . 'SCHEMA'] . ';charset=utf8';
    return new PDO($dsn, $config[$prefix . 'USER'], $config[$prefix . 'PASSWORD'],
        [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
};
$ai = $make('POC_AI_');
$store = $make('POC_');
$stage = 'database_identity';
foreach ([['pdo' => $ai, 'name' => $config['POC_AI_SCHEMA']],
    ['pdo' => $store, 'name' => $config['POC_SCHEMA']]] as $item) {
    $actual = $dialect === 'dm'
        ? [strtoupper((string)$item['pdo']->query('SELECT USER FROM DUAL')->fetchColumn()),
            strtoupper((string)$item['pdo']->query(
                "SELECT SYS_CONTEXT('USERENV','CURRENT_SCHEMA') FROM DUAL")->fetchColumn())]
        : [(string)$item['pdo']->query('SELECT DATABASE()')->fetchColumn()];
    $expected = $dialect === 'dm'
        ? [strtoupper($item['name']), strtoupper($item['name'])]
        : [$item['name']];
    if ($actual !== $expected) throw new RuntimeException('Connected database identity mismatch');
}

$required = [
    'ai' => [
        'YOSHOP_USER' => 'USER_ID,STORE_ID,NICK_NAME,MOBILE,BALANCE,GIFT_BALANCE,POINTS,IS_DELETE',
        'YOSHOP_STAFF' => 'ID,USER_ID,STORE_ID,HEIGHT,WEIGHT,GENDER,BIRTHDAY,IS_DELETE',
        'YOSHOP_USER_SUBSIDY' => 'ID,USER_ID,STORE_ID,MONEY,EXPIRATION_START_TIME,EXPIRATION_END_TIME,IS_DELETE',
        'YOSHOP_USER_OAUTH' => 'ID,USER_ID',
        'YOSHOP_USER_BALANCE_LOG' => 'LOG_ID,USER_ID,STORE_ID',
        'YOSHOP_WXAPP_SETTING' => null,
        'YOSHOP_STORE' => 'STORE_ID,IS_RECYCLE,IS_DELETE,LOGO_IMAGE_ID',
        'YOSHOP_STORE_SETTING' => null,
        'YOSHOP_UPLOAD_FILE' => 'FILE_ID,STORE_ID,FILE_TYPE,STORAGE,DOMAIN,FILE_PATH',
        'YOSHOP_USER_GRADE' => 'GRADE_ID,STORE_ID,IS_DELETE',
        'YOSHOP_USER_MOTION' => 'ID,USER_ID,DATE,MOVEMENT_STEPS',
    ],
    'store' => [
        'YDY_USER' => 'ID,AI_USER_ID,STAFF_UUID,PASSWORD_VERSION,IS_DELETE',
        'YDY_STAFF' => 'ID,UUID,AI_USER_ID,IS_SHOW',
        'YDY_STAFF_DEPARTMENT' => 'ID,STAFF_UUID,DEPARTMENT_UUID',
        'YDY_DEPARTMENT' => 'UUID,NAME,STATUS,DEPARTMENT_PATH',
        'YDY_CONFIG' => 'ID,CONFIG_KEY,CONFIG_VALUE,STATUS',
        'YDY_ONE_CARD_ACCOUNT_MAP' => 'ID,BUSINESS,USER_ID,STAFF_UUID,ACC_NUM',
    ],
];
$tableCounts = ['ai' => 0, 'store' => 0];
$columnCounts = ['ai' => 0, 'store' => 0];
$schemaGaps = [];
foreach ($required as $side => $tables) {
    $pdo = $side === 'ai' ? $ai : $store;
    foreach ($tables as $table => $columns) {
        $stage = 'schema_' . $side . '_' . strtolower($table);
        $name = $dialect === 'dm' ? $table : strtolower($table);
        $metaSql = $dialect === 'dm'
            ? 'SELECT COLUMN_NAME FROM USER_TAB_COLUMNS WHERE TABLE_NAME=?'
            : 'SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS'
                . ' WHERE TABLE_SCHEMA=? AND TABLE_NAME=?';
        $meta = $pdo->prepare($metaSql);
        $meta->execute($dialect === 'dm' ? [$name]
            : [$side === 'ai' ? $config['POC_AI_SCHEMA'] : $config['POC_SCHEMA'], $name]);
        $available = array_map('strtoupper', $meta->fetchAll(PDO::FETCH_COLUMN));
        if ($available === []) {
            $schemaGaps[$side][$table] = ['table_missing'];
            continue;
        }
        $requiredColumns = $columns === null ? [] : explode(',', $columns);
        $missing = array_values(array_diff($requiredColumns, $available));
        if ($missing !== []) {
            $schemaGaps[$side][$table] = $missing;
            continue;
        }
        $fieldSql = $requiredColumns === [] ? '1'
            : implode(',', array_map(static function (string $column) use ($dialect): string {
                return $dialect === 'dm' ? '"' . $column . '"' : '`' . $column . '`';
            }, $requiredColumns));
        $pdo->query('SELECT ' . $fieldSql . ' FROM ' . $name . ' WHERE 1=0');
        $tableCounts[$side]++;
        $columnCounts[$side] += count($requiredColumns);
    }
}

if ($schemaGaps !== []) {
    echo json_encode([
        'status' => 'schema_gap', 'target' => $target, 'dialect' => $dialect,
        'verified_table_groups' => $tableCounts,
        'missing_tables_or_columns' => $schemaGaps,
        'personal_rows_fetched' => 0, 'amount_values_fetched' => 0,
        'business_http_sent' => false, 'dml_sent' => false,
    ], JSON_UNESCAPED_SLASHES), PHP_EOL;
    exit(3);
}

$stage = 'synthetic_marker_absence';
$userTable = $dialect === 'dm' ? 'YOSHOP_USER' : 'yoshop_user';
$stmt = $ai->prepare('SELECT COUNT(*) FROM ' . $userTable
    . ' WHERE NICK_NAME LIKE ? AND STORE_ID=?');
$stmt->execute(['CDXPARREAD2%', 10001]);
$syntheticExistingCount = (int)$stmt->fetchColumn();
$staffTable = $dialect === 'dm' ? 'YOSHOP_STAFF' : 'yoshop_staff';
$staffStmt = $ai->prepare('SELECT COUNT(*) FROM ' . $staffTable
    . ' WHERE NAME LIKE ? AND STORE_ID=?');
$staffStmt->execute(['CDXPARREAD2%', 10001]);
$syntheticStaffCount = (int)$staffStmt->fetchColumn();
if ($syntheticExistingCount !== 0 || $syntheticStaffCount !== 0) {
    echo json_encode([
        'status' => 'synthetic_prefix_not_empty', 'target' => $target,
        'prefix_user_count' => $syntheticExistingCount,
        'prefix_staff_count' => $syntheticStaffCount,
        'business_http_sent' => false, 'dml_sent' => false,
    ], JSON_UNESCAPED_SLASHES), PHP_EOL;
    exit(4);
}

echo json_encode([
    'status' => 'read2_schema_preflight_pass', 'target' => $target,
    'dialect' => $dialect, 'ai_and_store_identity_match' => true,
    'table_groups' => $tableCounts, 'required_column_counts' => $columnCounts,
    'synthetic_prefix_existing_count' => 0,
    'personal_rows_fetched' => 0, 'amount_values_fetched' => 0,
    'business_http_sent' => false, 'dml_sent' => false,
], JSON_UNESCAPED_SLASHES), PHP_EOL;
