<?php
declare(strict_types=1);

// Offline draft. No approval + exact SHA means no runtime, DB or cache access.
if (!hash_equals('CDX_CAT8_PREFLIGHT_20261002_V1',
    (string)getenv('CDX_CAT8_PREFLIGHT_APPROVAL'))) {
    echo json_encode(['status' => 'default_deny', 'database_connected' => false,
        'cache_accessed' => false, 'business_http_sent' => false,
        'business_dml_sent' => false]), PHP_EOL;
    exit(78);
}

$stage = 'source_review';
$redactedError = static function (Throwable $error) use (&$stage): void {
    echo json_encode(['status' => 'redacted_error', 'stage' => $stage,
        'class' => get_class($error), 'business_dml_sent' => false]), PHP_EOL;
    exit(2);
};
set_exception_handler($redactedError);
set_error_handler(static function (int $severity): bool {
    if (!(error_reporting() & $severity)) return false;
    throw new ErrorException('redacted_warning', 0, $severity);
});

if (!hash_equals((string)hash_file('sha256', __FILE__),
        (string)getenv('CDX_CAT8_PREFLIGHT_SHA256'))) {
    throw new RuntimeException('Exact reviewed script SHA required');
}
$mode = (string)getenv('CDX_CAT8_PREFLIGHT_MODE');
if (!in_array($mode, ['schema', 'cache_probe', 'cache_zero'], true)) {
    throw new RuntimeException('Explicit preflight mode required');
}
$target = (string)getenv('CDX_CAT8_PREFLIGHT_TARGET');
if (!in_array($target, ['A', 'B', 'C'], true)) {
    throw new RuntimeException('Explicit A/B/C target required');
}
$shared = __DIR__ . '/../parallel-ai-catalog8-fast-super-20261002/common.php';
if (!hash_equals('84559b3336754028e63ab4c8950ada61d6e752c60266be1243f2b660ee893511',
        (string)hash_file('sha256', $shared))) {
    throw new RuntimeException('Pinned Catalog8 common changed');
}
require $shared;

// This also checks the pinned old library, exact isolated process/docroot,
// local-dev configuration (A/B), nonproduction DM (C), actual schema,
// external stubs and private File Cache. Its sole HTTP call is technical
// /__poc_boundary, never a business endpoint.
$stage = 'isolated_runtime';
try {
    $ctx = cat8_context($target);
} catch (Throwable $error) {
    $redactedError($error);
}
// Think App initialization installs its own exception handler; restore the
// redacted test handler before metadata/cache work can fail.
set_exception_handler($redactedError);
/** @var PDO $db */
$db = $ctx['pdo'];

if ($mode === 'schema') {
    $stage = 'schema_metadata';
    $table = 'yoshop_store_user';
    $required = ['STORE_USER_ID', 'USER_NAME', 'PASSWORD', 'REAL_NAME',
        'IS_SUPER', 'IS_DELETE', 'STORE_ID', 'CREATE_TIME', 'UPDATE_TIME',
        'NODE', 'CREATE_BY'];
    if ($target === 'C') {
        $columns = cat8_rows($db, 'SELECT COLUMN_NAME, NULLABLE, DATA_DEFAULT, '
            . 'DATA_TYPE FROM USER_TAB_COLUMNS '
            . 'WHERE TABLE_NAME=?', [strtoupper($table)]);
        // The business account cannot read SYS.SYSCOLUMNS. Use only its own
        // metadata view; if IDENTITY_COLUMN is unavailable, keep the identity
        // gate explicitly unresolved until a controlled one-row trial.
        $dmIdAuto = null;
        try {
            $identityRows = cat8_rows($db, 'SELECT IDENTITY_COLUMN FROM USER_TAB_COLUMNS'
                . ' WHERE TABLE_NAME=? AND COLUMN_NAME=?',
                [strtoupper($table), 'STORE_USER_ID']);
            if (count($identityRows) === 1) {
                $flag = strtoupper((string)$identityRows[0]['identity_column']);
                $dmIdAuto = $flag === 'YES' || $flag === 'Y';
            }
        } catch (Throwable $metadataError) {
            $dmIdAuto = null;
        }
        $triggers = cat8_count($db, 'SELECT COUNT(*) FROM USER_TRIGGERS '
            . 'WHERE TABLE_NAME=?', [strtoupper($table)]);
        $outgoing = cat8_count($db, 'SELECT COUNT(*) FROM USER_CONSTRAINTS '
            . "WHERE TABLE_NAME=? AND CONSTRAINT_TYPE='R'", [strtoupper($table)]);
        $incoming = cat8_count($db, 'SELECT COUNT(*) FROM USER_CONSTRAINTS '
            . "WHERE CONSTRAINT_TYPE='R' AND R_CONSTRAINT_NAME IN "
            . "(SELECT CONSTRAINT_NAME FROM USER_CONSTRAINTS WHERE TABLE_NAME=? "
            . "AND CONSTRAINT_TYPE IN ('P','U'))", [strtoupper($table)]);
    } else {
        $columns = cat8_rows($db, 'SELECT COLUMN_NAME, IS_NULLABLE AS NULLABLE, '
            . 'COLUMN_DEFAULT AS DATA_DEFAULT, DATA_TYPE, EXTRA AS IDENTITY_COLUMN '
            . 'FROM INFORMATION_SCHEMA.COLUMNS '
            . 'WHERE TABLE_SCHEMA=? AND TABLE_NAME=?', [$ctx['schema'], $table]);
        $triggers = cat8_count($db, 'SELECT COUNT(*) FROM INFORMATION_SCHEMA.TRIGGERS '
            . 'WHERE TRIGGER_SCHEMA=? AND EVENT_OBJECT_TABLE=?', [$ctx['schema'], $table]);
        $outgoing = cat8_count($db, 'SELECT COUNT(*) FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE '
            . 'WHERE TABLE_SCHEMA=? AND TABLE_NAME=? AND REFERENCED_TABLE_NAME IS NOT NULL',
            [$ctx['schema'], $table]);
        $incoming = cat8_count($db, 'SELECT COUNT(*) FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE '
            . 'WHERE REFERENCED_TABLE_SCHEMA=? AND REFERENCED_TABLE_NAME=?',
            [$ctx['schema'], $table]);
    }
    $byName = [];
    foreach ($columns as $column) {
        $name = strtoupper((string)$column['column_name']);
        if (isset($byName[$name])) throw new RuntimeException('Duplicate column metadata');
        $byName[$name] = $column;
    }
    if ($target === 'C' && isset($byName['STORE_USER_ID'])) {
        $byName['STORE_USER_ID']['identity_column'] = $dmIdAuto ? 'YES' : 'NO';
    }
    $missing = array_values(array_diff($required, array_keys($byName)));
    $insertFields = array_flip(array_slice($required, 1));
    $noDefault = [];
    foreach ($byName as $name => $column) {
        if (isset($insertFields[$name])
            || ($target === 'C' && $name === 'STORE_USER_ID' && $dmIdAuto === null)) {
            continue;
        }
        $nullable = strtoupper((string)$column['nullable']) === 'YES'
            || strtoupper((string)$column['nullable']) === 'Y';
        $default = $column['data_default'];
        $identity = strtoupper((string)($column['identity_column'] ?? ''));
        $isIdentity = $identity === 'YES' || $identity === 'Y'
            || strpos(strtolower($identity), 'auto_increment') !== false;
        if (!$nullable && ($default === null || trim((string)$default) === '')
            && !$isIdentity) {
            $noDefault[] = $name;
        }
    }
    // The next fixture inserts one row without ID and relies on an identity.
    $idMeta = $byName['STORE_USER_ID'] ?? [];
    $idIdentity = strtoupper((string)($idMeta['identity_column'] ?? ''));
    $idAuto = ($target === 'C' && $dmIdAuto === true)
        || $idIdentity === 'YES' || $idIdentity === 'Y'
        || strpos(strtolower($idIdentity), 'auto_increment') !== false;
    $stage = 'marker_and_reference_preflight';
    $markerCount = cat8_count($db,
        "SELECT COUNT(*) FROM yoshop_store_user WHERE USER_NAME LIKE 'CDXCAT8U%'");
    $structuralReady = count($columns) > 0 && !$missing && !$noDefault
        && $triggers === 0 && $outgoing === 0 && $incoming === 0
        && $markerCount === 0;
    $status = !$structuralReady ? 'schema_blocked'
        : ($idAuto ? 'schema_ready' : 'schema_identity_unverified');
    echo json_encode(['status' => $status,
        'target' => $target, 'dialect' => $target === 'C' ? 'dm' : 'mysql',
        'schema' => $ctx['schema'], 'column_count' => count($columns),
        'missing' => $missing, 'nonnullable_without_default' => $noDefault,
        'id_identity' => $idAuto, 'id_identity_state' => $target === 'C'
            ? ($dmIdAuto === null ? 'unknown' : ($dmIdAuto ? 'yes' : 'no'))
            : ($idAuto ? 'yes' : 'no'),
        'triggers' => $triggers,
        'outgoing_fk' => $outgoing, 'incoming_fk' => $incoming,
        'catalog8_marker_count' => $markerCount, 'business_dml_sent' => false]), PHP_EOL;
    exit($status === 'schema_ready' ? 0 : 3);
}

// A cache probe is unrelated to account creation: random key only, private
// cache only, 300-second header, delete in finally. A second PHP process must
// call cache_zero against the private journal to prove file absence.
$stage = 'cache_journal';
if ($mode === 'cache_probe') {
    $directory = '/tmp/cdx-cat8-preflight-' . bin2hex(random_bytes(8));
    if (!mkdir($directory, 0700)) throw new RuntimeException('Private journal directory failed');
    $journal = $directory . '/manifest.json';
    $key = 'CDXCAT8PROBE' . strtoupper(bin2hex(random_bytes(16)));
    $data = ['target' => $target, 'key' => $key, 'status' => 'pending',
        'created_at' => time()];
    if (file_put_contents($journal, json_encode($data), LOCK_EX) === false
        || !chmod($journal, 0600)) {
        throw new RuntimeException('Private cache journal failed');
    }
    $driver = \think\facade\Cache::store('file');
    $path = $driver->getCacheKey($key);
    $cacheRoot = realpath($ctx['root'] . '/runtime/cache');
    if (!is_string($cacheRoot) || strpos($path, $cacheRoot . '/') !== 0
        || is_file($path)) {
        throw new RuntimeException('Probe path not isolated or occupied');
    }
    $stage = 'cache_probe';
    try {
        $value = 'CAT8_PROBE_' . bin2hex(random_bytes(8));
        if (!$driver->set($key, $value, 300)) {
            throw new RuntimeException('Private cache set failed');
        }
        $ttl = cat8_cache_ttl($key);
        if ($driver->get($key) !== $value || $ttl['expires_at'] < time() + 280) {
            throw new RuntimeException('Private cache read/TTL failed');
        }
    } finally {
        $driver->delete($key);
    }
    clearstatcache(true, $path);
    if (is_file($path)) throw new RuntimeException('Private cache delete not proven');
    $data['status'] = 'deleted_pending_independent_zero';
    if (file_put_contents($journal, json_encode($data), LOCK_EX) === false) {
        throw new RuntimeException('Private cache journal update failed');
    }
    echo json_encode(['status' => 'cache_probe_deleted', 'target' => $target,
        'journal_path' => $journal, 'ttl_seconds' => 300,
        'business_dml_sent' => false]), PHP_EOL;
    exit(0);
}

$journal = (string)getenv('CDX_CAT8_PREFLIGHT_JOURNAL');
$resolved = realpath($journal);
if (!is_string($resolved)
    || !preg_match('#^/(?:private/)?tmp/cdx-cat8-preflight-[a-f0-9]{16}/manifest\.json$#', $resolved)
    || (fileperms($resolved) & 0077) !== 0
    || (fileperms(dirname($resolved)) & 0077) !== 0) {
    throw new RuntimeException('Exact private journal required');
}
$data = json_decode((string)file_get_contents($resolved), true);
if (!is_array($data) || ($data['target'] ?? null) !== $target
    || ($data['status'] ?? null) !== 'deleted_pending_independent_zero'
    || !preg_match('/^CDXCAT8PROBE[A-F0-9]{32}$/', (string)($data['key'] ?? ''))) {
    throw new RuntimeException('Cache journal identity mismatch');
}
$stage = 'independent_cache_zero';
$path = \think\facade\Cache::store('file')->getCacheKey($data['key']);
$cacheRoot = realpath($ctx['root'] . '/runtime/cache');
if (!is_string($cacheRoot) || strpos($path, $cacheRoot . '/') !== 0) {
    throw new RuntimeException('Probe path no longer isolated');
}
clearstatcache(true, $path);
if (is_file($path)) throw new RuntimeException('Probe cache file remains');
$data['status'] = 'independent_zero';
if (file_put_contents($resolved, json_encode($data), LOCK_EX) === false) {
    throw new RuntimeException('Private cache journal update failed');
}
echo json_encode(['status' => 'cache_independent_zero', 'target' => $target,
    'business_dml_sent' => false]), PHP_EOL;
