<?php
declare(strict_types=1);

/**
 * Exact B-isolated-config recovery. Default deny. Never run --execute without
 * a separately approved maintenance window, this file's SHA and manifest SHA.
 *
 * PHP rename($temp, $target) can overwrite a concurrently created target.
 * This uses a same-directory temp file and link($temp, $target): link is an
 * atomic no-replace publication on the same filesystem. Only this run's temp
 * name is removed after successful publication; failure leaves the journal
 * and temp for exact recovery. No original file or test data is deleted.
 */

set_exception_handler(static function (Throwable $e): void {
    echo json_encode(['status' => 'restore-halt', 'class' => get_class($e),
        'code' => (string)$e->getCode(),
        'message_sha256' => hash('sha256', $e->getMessage())], JSON_UNESCAPED_SLASHES), PHP_EOL;
    exit(3);
});

$mode = (string)($argv[1] ?? '');
if (!in_array($mode, ['--audit', '--verify', '--execute'], true) || count($argv) !== 2) {
    fwrite(STDERR, "DENIED: exact --audit, --verify, or separately approved --execute required\n");
    exit(2);
}
if ($mode === '--execute') {
    $selfSha = hash_file('sha256', __FILE__);
    $manifestSha = hash_file('sha256', __DIR__ . '/B_MISSING_CONFIG_22_MANIFEST.tsv');
    if (getenv('CDX_B_CONFIG_RESTORE_APPROVED') !== 'B_CONFIG_EXACT_22_V1'
        || getenv('CDX_B_CONFIG_EXCLUSIVE_HTTP_WINDOW') !== '1'
        || !hash_equals($selfSha, (string)getenv('CDX_B_CONFIG_RESTORE_SCRIPT_SHA'))
        || !hash_equals($manifestSha, (string)getenv('CDX_B_CONFIG_RESTORE_MANIFEST_SHA'))) {
        fwrite(STDERR, "DENIED: reviewed SHAs and exclusive maintenance approval required\n");
        exit(2);
    }
}

const B_PID = 13127;
const B_PORT = 18101;
const B_SOCKET_TOKEN = '0xc1b5193ecc2ef2dc';
const B_ROOT = '/private/tmp/zhct-http-ai-macos.x2CCZX';
const B_CONFIG = B_ROOT . '/config';
const B_SOURCE = '/Users/wiscourper/Documents/workspace/local/dm8-full-acceptance.fF5yTy/ai_api';
const B_DB_SHA = '8c46a6d9cd277f2950818d09162f639a62f135a5eb6553e6c61ce2c19167fd02';
const B_BANK_SHA = '5a0569974c11672eb3a62ef6f290a2f2cda5ba7165888f71c5905e8c3eab692b';
const B_BANK_SERVICE_SHA = 'acb09ee3c5d9e75d1b7d677d97e30c4b4bc8804872d0632fbaae60fc68054649';
const B_STUB_SHA = 'ed0ea9f23c70e504cf5331b7a95fe2a9fd6704beabf254a474efbe8ecb4d59a0';
const B_CACHE_SHA = 'ccf1c56eb4178f3eaad8775e26a27742b2cacf662b46fb058cbb91d23783b8c6';
const B_LOG_SHA = '16025200f4a396d32a181151da621a017352c1e8c648fe3c84e66c961d878e9a';
const B_STATUS_SHA = '77f375df24fae1a2e010cca1aa89cb646b92df7690c7253472ef06385e3af446';
const DEV_AI_DB_SHA = '210f925e093f71e26a8c8923f0f626e3be0a15ccff3d386bba21feef695c8a5b';
const DEV_STORE_DB_SHA = '003eb5aecf1cd4595b609c1f7f20b9ee325b578309fb9fe14fedaf77584fa401';

const FILES = [
    'app.php', 'baidu_face.php', 'business.php', 'cache.php', 'captcha.php',
    'console.php', 'cookie.php', 'filesystem.php', 'gateway_worker.php',
    'lang.php', 'log.php', 'message.php', 'middleware.php', 'queue.php',
    'route.php', 'session.php', 'shanghe.php', 'status.php', 'trace.php',
    'view.php', 'worker.php', 'worker_server.php',
];

function fileMode(string $path): int
{
    $mode = @fileperms($path);
    if ($mode === false) throw new RuntimeException('required path mode unavailable');
    return $mode & 0777;
}

function exactManifest(): array
{
    $path = __DIR__ . '/B_MISSING_CONFIG_22_MANIFEST.tsv';
    $rows = file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
    if (!is_array($rows) || array_shift($rows) !== "filename\tsha256\tsource_mode\tapproved_target_mode"
        || count($rows) !== 22) throw new RuntimeException('reviewed manifest shape drift');
    $entries = [];
    foreach ($rows as $line) {
        $parts = explode("\t", $line);
        if (count($parts) !== 4 || !preg_match('/^[a-z_]+\.php$/', $parts[0])
            || !preg_match('/^[a-f0-9]{64}$/', $parts[1])
            || $parts[2] !== '0644' || $parts[3] !== '0644'
            || isset($entries[$parts[0]]) || $parts[0] === 'database.php') {
            throw new RuntimeException('reviewed manifest content drift');
        }
        $entries[$parts[0]] = ['sha' => $parts[1], 'mode' => 0644];
    }
    if (array_keys($entries) !== FILES) throw new RuntimeException('22-file allowlist drift');
    return $entries;
}

function noHttpInFlight(): bool
{
    // A point-in-time check, not a substitute for the approved exclusive window.
    $connected = trim((string)shell_exec(
        '/usr/sbin/lsof -nP -iTCP:' . B_PORT . ' -sTCP:ESTABLISHED -t 2>/dev/null'));
    return $connected === '';
}

function fixedBoundary(): void
{
    if (PHP_VERSION_ID < 70400 || PHP_VERSION_ID >= 70500 || PHP_OS_FAMILY !== 'Darwin') {
        throw new RuntimeException('approved Mac PHP7.4 required');
    }
    $owners = trim((string)shell_exec(
        '/usr/sbin/lsof -nP -iTCP:' . B_PORT . ' -sTCP:LISTEN -t 2>/dev/null'));
    $socket = (string)shell_exec(
        '/usr/sbin/lsof -nP -iTCP:' . B_PORT . ' -sTCP:LISTEN 2>/dev/null');
    $command = (string)shell_exec('/bin/ps eww -p ' . B_PID . ' -o command=');
    if ($owners !== (string)B_PID || substr_count($socket, '127.0.0.1:' . B_PORT) !== 1
        || strpos($socket, '0.0.0.0:' . B_PORT) !== false
        || strpos($socket, B_SOCKET_TOKEN) === false
        || strpos($command, '-S 127.0.0.1:' . B_PORT) === false
        || (strpos($command, B_ROOT . '/public') === false
            && strpos($command, str_replace('/private/tmp/', '/tmp/', B_ROOT) . '/public') === false)
        || strpos($command, 'PHP_CLI_SERVER_WORKERS=') !== false) {
        throw new RuntimeException('fixed B PID/socket or single-worker boundary drift');
    }
    if (!is_dir(B_ROOT) || is_link(B_ROOT) || fileMode(B_ROOT) !== 0700
        || realpath(B_SOURCE) !== B_SOURCE
        || !is_dir(B_SOURCE . '/config') || is_link(B_SOURCE . '/config')
        || !is_dir(B_CONFIG) || is_link(B_CONFIG) || fileMode(B_CONFIG) !== 0755
        || realpath(B_CONFIG) !== B_CONFIG
        || !is_dir(B_ROOT . '/runtime')
        || realpath(B_ROOT . '/app/api/controller/Bank.php')
            !== realpath(B_SOURCE . '/app/api/controller/Bank.php')
        || hash_file('sha256', B_ROOT . '/app/api/controller/Bank.php') !== B_BANK_SHA
        || hash_file('sha256', B_ROOT . '/app/api/service/Bank.php') !== B_BANK_SERVICE_SHA
        || hash_file('sha256', B_ROOT . '/external_stubs.php') !== B_STUB_SHA
        || !is_file(B_CONFIG . '/database.php') || is_link(B_CONFIG . '/database.php')
        || fileMode(B_CONFIG . '/database.php') !== 0644
        || hash_file('sha256', B_CONFIG . '/database.php') !== B_DB_SHA) {
        throw new RuntimeException('B root, Composer source, stub or preserved database config drift');
    }
    $log = '/private/tmp/zhct-ai-http-log.yaHfja/B-18101.log';
    $fds = (string)shell_exec('/usr/sbin/lsof -a -p ' . B_PID . ' -d 1,2 -Fn 2>/dev/null');
    if (!is_dir(dirname($log)) || fileMode(dirname($log)) !== 0700
        || !is_file($log) || is_link($log) || fileMode($log) !== 0600
        || substr_count($fds, 'n' . $log) !== 2) {
        throw new RuntimeException('private B stdout/stderr log boundary drift');
    }
    // Secrets are compared in memory only, never printed or journaled.
    $config = parse_ini_file(
        '/Users/wiscourper/Documents/workspace/zhct/zhctproject/ai_api/.env.local',
        true, INI_SCANNER_RAW);
    if (!is_array($config) || !isset($config['DATABASE'], $config['DATABASE_ZHCT'])) {
        throw new RuntimeException('two code-local development database sections unavailable');
    }
    foreach (['DATABASE' => 'POC_AI_', 'DATABASE_ZHCT' => 'POC_'] as $section => $prefix) {
        foreach (['HOSTNAME' => 'HOST', 'HOSTPORT' => 'PORT', 'DATABASE' => 'SCHEMA',
            'USERNAME' => 'USER', 'PASSWORD' => 'PASSWORD'] as $source => $suffix) {
            $value = (string)($config[$section][$source] ?? '');
            if ($value === '' || preg_match('/\s/', $value)
                || preg_match('/(?:^|\s)' . preg_quote($prefix . $suffix . '=' . $value, '/')
                    . '(?=\s|$)/', $command) !== 1) {
                throw new RuntimeException('B process differs from code-local development database');
            }
        }
    }
    foreach (['POC_DIALECT=mysql', 'POC_EXTERNAL_STUBS=1'] as $required) {
        if (preg_match('/(?:^|\s)' . preg_quote($required, '/') . '(?=\s|$)/', $command) !== 1) {
            throw new RuntimeException('B mysql/stub environment drift');
        }
    }
    if (!noHttpInFlight()) throw new RuntimeException('B HTTP request currently in flight');
}

function verifySourceAndTarget(array $entries, bool $expectPresent): array
{
    $actualNames = array_values(array_diff(scandir(B_CONFIG) ?: [], ['.', '..']));
    sort($actualNames);
    $expectedNames = $expectPresent ? array_merge(FILES, ['database.php']) : ['database.php'];
    sort($expectedNames);
    if ($actualNames !== $expectedNames) {
        throw new RuntimeException('B config directory exact object inventory drift');
    }
    $tree = [];
    foreach ($entries as $name => $entry) {
        $source = B_SOURCE . '/config/' . $name;
        $target = B_CONFIG . '/' . $name;
        if (!is_file($source) || is_link($source) || fileMode($source) !== 0644
            || !hash_equals($entry['sha'], hash_file('sha256', $source))) {
            throw new RuntimeException('reviewed B source file hash or mode drift');
        }
        $exists = file_exists($target) || is_link($target);
        if ($exists !== $expectPresent) throw new RuntimeException('B target existence drift');
        if ($expectPresent) {
            if (!is_file($target) || is_link($target) || fileMode($target) !== $entry['mode']
                || !hash_equals($entry['sha'], hash_file('sha256', $target))) {
                throw new RuntimeException('restored B target hash or mode drift');
            }
            $tree[] = $name . "\t" . $entry['sha'] . "\t0644";
        }
    }
    if ($expectPresent) {
        $tree[] = 'database.php' . "\t" . B_DB_SHA . "\t" . sprintf('%04o', fileMode(B_CONFIG . '/database.php'));
        sort($tree);
    }
    return $tree;
}

function databaseReadback(): array
{
    $config = parse_ini_file(
        '/Users/wiscourper/Documents/workspace/zhct/zhctproject/ai_api/.env.local',
        true, INI_SCANNER_RAW);
    $checks = [];
    foreach (['DATABASE' => DEV_AI_DB_SHA, 'DATABASE_ZHCT' => DEV_STORE_DB_SHA] as $section => $expected) {
        $c = $config[$section];
        if (strtolower((string)($c['TYPE'] ?? '')) !== 'mysql') {
            throw new RuntimeException('development connection dialect drift');
        }
        $db = new PDO('mysql:host=' . $c['HOSTNAME'] . ';port=' . (int)$c['HOSTPORT']
            . ';dbname=' . $c['DATABASE'] . ';charset=' . ($c['CHARSET'] ?? 'utf8'),
            $c['USERNAME'], $c['PASSWORD'], [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
                PDO::ATTR_TIMEOUT => 5]);
        $identity = (string)$db->query(
            "SELECT CONCAT(@@server_uuid,'|',@@hostname,'|',@@port,'|',DATABASE())")
            ->fetchColumn();
        if (!hash_equals($expected, hash('sha256', $identity))) {
            throw new RuntimeException('development database identity changed');
        }
        $checks[$section] = true;
    }
    return $checks;
}

function appendJournal($handle, array $event): void
{
    $json = json_encode($event, JSON_UNESCAPED_SLASHES);
    if ($json === false) throw new RuntimeException('private restore journal encoding failed');
    $line = $json . "\n";
    if (fwrite($handle, $line) !== strlen($line) || !fflush($handle)) {
        throw new RuntimeException('private restore journal write failed');
    }
}

$entries = exactManifest();
fixedBoundary();
verifySourceAndTarget($entries, $mode === '--verify');
if ($mode === '--audit') {
    echo json_encode(['status' => 'B-config-22-readonly-audit-pass', 'pid' => B_PID,
        'port' => B_PORT, 'source_files' => 22, 'target_files_present' => 0,
        'preserved_database_sha256' => B_DB_SHA,
        'manifest_sha256' => hash_file('sha256', __DIR__ . '/B_MISSING_CONFIG_22_MANIFEST.tsv')],
        JSON_UNESCAPED_SLASHES), PHP_EOL;
    exit(0);
}
if ($mode === '--verify') {
    $tree = verifySourceAndTarget($entries, true);
    if (hash_file('sha256', B_CONFIG . '/cache.php') !== B_CACHE_SHA
        || hash_file('sha256', B_CONFIG . '/log.php') !== B_LOG_SHA
        || hash_file('sha256', B_CONFIG . '/status.php') !== B_STATUS_SHA) {
        throw new RuntimeException('cache/log/status agreed hashes changed');
    }
    $db = databaseReadback();
    echo json_encode(['status' => 'B-config-22-readonly-verify-pass', 'pid' => B_PID,
        'port' => B_PORT, 'config_tree_sha256' => hash('sha256', implode("\n", $tree)),
        'preserved_database_sha256' => B_DB_SHA, 'private_log_and_stub' => true,
        'code_local_development_databases' => $db,
        'same_pid_files_visible_not_business_http_proven' => true],
        JSON_UNESCAPED_SLASHES), PHP_EOL;
    exit(0);
}

// Mutation starts only below this point. The run's private append-only journal
// is deliberately retained on both success and failure; never auto-rollback.
$started = time();
$nonce = bin2hex(random_bytes(12));
$journalPath = B_ROOT . '/runtime/cdx-b-config-restore-' . $nonce . '.ndjson';
$journal = @fopen($journalPath, 'x');
if ($journal === false) throw new RuntimeException('private restore journal cannot be created');
chmod($journalPath, 0600);
appendJournal($journal, ['event' => 'start', 'pid' => B_PID, 'port' => B_PORT,
    'planned_files' => array_keys($entries), 'preserved_database_sha256' => B_DB_SHA,
    'manifest_sha256' => hash_file('sha256', __DIR__ . '/B_MISSING_CONFIG_22_MANIFEST.tsv')]);
foreach ($entries as $name => $entry) {
    fixedBoundary();
    $source = B_SOURCE . '/config/' . $name;
    $target = B_CONFIG . '/' . $name;
    if (file_exists($target) || is_link($target)
        || hash_file('sha256', $source) !== $entry['sha']) {
        appendJournal($journal, ['event' => 'stop-target-or-source-drift', 'file' => $name]);
        throw new RuntimeException('refuse overwrite or changed source');
    }
    appendJournal($journal, ['event' => 'staging', 'file' => $name]);
    $temp = B_CONFIG . '/.' . $name . '.cdx-' . $nonce . '.tmp';
    $input = @fopen($source, 'rb');
    $output = @fopen($temp, 'x');
    if ($input === false || $output === false) {
        appendJournal($journal, ['event' => 'stop-temp-create', 'file' => $name]);
        throw new RuntimeException('source or exclusive temp open failed');
    }
    while (!feof($input)) {
        $chunk = fread($input, 65536);
        if ($chunk === false || ($chunk === '' && !feof($input))
            || ($chunk !== '' && fwrite($output, $chunk) !== strlen($chunk))) {
            throw new RuntimeException('exact source-to-temp copy failed');
        }
    }
    fclose($input);
    if (!fflush($output)) throw new RuntimeException('temp flush failed');
    fclose($output);
    chmod($temp, $entry['mode']);
    // New mtime is intentional. Do not use cp -p or inherit September mtime.
    if (!touch($temp, time())) throw new RuntimeException('fresh target mtime failed');
    if (fileMode($temp) !== $entry['mode']
        || filemtime($temp) < $started
        || !hash_equals($entry['sha'], hash_file('sha256', $temp))) {
        appendJournal($journal, ['event' => 'stop-temp-verify', 'file' => $name]);
        throw new RuntimeException('same-directory temp hash/mode/new-mtime mismatch');
    }
    if (file_exists($target) || is_link($target)
        || !@link($temp, $target)) {
        appendJournal($journal, ['event' => 'stop-atomic-no-replace', 'file' => $name,
            'temp_basename' => basename($temp)]);
        throw new RuntimeException('atomic target exists or no-replace publish failed');
    }
    // Only the temp created by this run is removed; target and journal remain.
    if (!@unlink($temp)) {
        appendJournal($journal, ['event' => 'published-temp-retained', 'file' => $name,
            'temp_basename' => basename($temp)]);
        throw new RuntimeException('published but exact temp cleanup failed');
    }
    appendJournal($journal, ['event' => 'published', 'file' => $name,
        'sha256' => $entry['sha'], 'mode' => '0644', 'mtime' => filemtime($target)]);
}
$tree = verifySourceAndTarget($entries, true);
foreach ($entries as $name => $entry) {
    if (filemtime(B_CONFIG . '/' . $name) < $started) {
        throw new RuntimeException('restored file mtime became old');
    }
}
fixedBoundary();
$db = databaseReadback();
$treeSha = hash('sha256', implode("\n", $tree));
appendJournal($journal, ['event' => 'verified', 'config_tree_sha256' => $treeSha,
    'same_pid' => B_PID, 'code_local_dev_db_ok' => $db]);
fclose($journal);
echo json_encode(['status' => 'B-config-22-published-but-no-business-HTTP',
    'pid' => B_PID, 'restored_files' => 22, 'config_tree_sha256' => $treeSha,
    'journal_basename' => basename($journalPath),
    'preserved_database_sha256' => B_DB_SHA], JSON_UNESCAPED_SLASHES), PHP_EOL;
