<?php
declare(strict_types=1);

// Process-crash recovery for proven pre-HTTP fixtures or already-zero cleanup.
// Unknown/in-flight HTTP and post-HTTP remaining rows are never auto-cleaned.
$lane = (string)getenv('CDX_EM_RACE_LANE');
$journalPath = (string)getenv('CDX_EM_RACE_JOURNAL');
if (!in_array($lane, ['A', 'B', 'C'], true)
    || count($argv) !== 2 || ($argv[1] ?? '') !== '--recover-race'
    || (string)getenv('CDX_EM_RACE_WRITE_RECOVER')
        !== 'ROOT_APPROVED_EXACT_RACE_RECOVERY_V1'
    || !hash_equals((string)hash_file('sha256', __FILE__),
        (string)getenv('CDX_EM_RACE_WRITE_RECOVER_SHA'))
    || !preg_match('~^/tmp/cdx-em-race-barrier-[a-f0-9]{12}/recovery\.json$~D',
        $journalPath)) {
    echo json_encode(['status' => 'default-deny', 'db_connected' => false,
        'business_http_sent' => false, 'dml_sent' => false]), PHP_EOL;
    exit(78);
}
const RACE_WRITE_RECOVERY_REVIEWED = false;
if (!RACE_WRITE_RECOVERY_REVIEWED) {
    echo json_encode(['status' => 'recovery-review-blocked',
        'db_connected' => false, 'business_http_sent' => false,
        'dml_sent' => false]), PHP_EOL;
    exit(78);
}
$stage = 'journal_identity';
set_exception_handler(static function (Throwable $error) use (&$stage): void {
    echo json_encode(['status' => 'redacted_stop', 'stage' => $stage,
        'error_class' => get_class($error), 'code' => (string)$error->getCode(),
        'reason_sha' => hash('sha256', $error->getMessage()),
        'journal_retained' => true]), PHP_EOL;
    exit(2);
});
if (is_link($journalPath) || realpath($journalPath) !== $journalPath
    || is_link(dirname($journalPath))
    || (((int)fileperms(dirname($journalPath)) & 0077) !== 0)
    || (((int)fileperms($journalPath) & 0077) !== 0)) {
    throw new RuntimeException('Private journal path unsafe');
}
$journal = json_decode((string)file_get_contents($journalPath), true);
if (!is_array($journal) || ($journal['lane'] ?? null) !== $lane
    || !is_array($journal['source_before'] ?? null)
    || array_keys($journal['source_before'])
        !== ['electronic_menu', 'electronic_menu_bind_code',
            'electronic_menu_device', 'electronic_menu_device_log']) {
    throw new RuntimeException('Recovery journal shape invalid');
}
$policy = __DIR__ . '/race_write_recovery_policy.php';
if (!hash_equals('3a02d16edd9672e7431354d1302fe4cfa58b7376467b4ec3a589c50f36ebd26d',
    (string)hash_file('sha256', $policy))) {
    throw new RuntimeException('Reviewed recovery policy changed');
}
require_once $policy;
$lock = fopen(sys_get_temp_dir() . '/cdx-em-race-'
    . ($lane === 'C' ? 'c' : 'dev') . '.lock', 'c');
if ($lock === false || !flock($lock, LOCK_EX | LOCK_NB)) {
    throw new RuntimeException('Same-DB recovery writer lock unavailable');
}
$stage = 'approved_source_and_database';
$probe = $lane === 'C'
    ? '/tmp/zhct-emenu-core.1ZcUkR/preflight_log_batch.php'
    : __DIR__ . '/../parallel-electronic-menu-log-batch-20261004/preflight_log_batch.php';
if (!hash_equals('8c619b96c447f849b551b5c7119007fee90a34d9ce623eb05e9027280a87fef9',
    (string)hash_file('sha256', $probe))) {
    throw new RuntimeException('Approved Store environment probe changed');
}
putenv('CDX_EM_LOG_LANE=' . $lane);
putenv('CDX_EM_LOG_PREFLIGHT=OWNED_DEVICE_LOG_BATCH_SELECT_ONLY_V1');
putenv('CDX_EM_LOG_PREFLIGHT_SHA=8c619b96c447f849b551b5c7119007fee90a34d9ce623eb05e9027280a87fef9');
$outerArgv = $argv;
$argv[1] = '--preflight-only';
ob_start();
require $probe;
$probeText = (string)ob_get_clean();
$argv = $outerArgv;
restore_exception_handler();
$probeResult = json_decode(trim($probeText), true);
if (!is_array($probeResult) || ($probeResult['status'] ?? null) !== 'select_only_pass'
    || ($probeResult['lane'] ?? null) !== $lane) {
    throw new RuntimeException('Approved source or database identity changed');
}
$name = static function (string $short) use ($dialect): string {
    if (!in_array($short, ['electronic_menu', 'electronic_menu_bind_code',
        'electronic_menu_device', 'electronic_menu_device_log'], true)) {
        throw new RuntimeException('Recovery table not allowlisted');
    }
    return $dialect === 'dm' ? strtoupper('ydy_' . $short) : 'ydy_' . $short;
};
$marker = (string)($journal['marker'] ?? '');
$menuUuid = (string)($journal['menu_uuid'] ?? '');
$token = (string)($journal['public_token'] ?? '');
$codeHash = (string)($journal['code_hash'] ?? '');
$stage = 'exact_owned_observation';
$q = $db->prepare('SELECT ID,UUID,PUBLIC_TOKEN,CREATE_BY,STORE_ID FROM '
    . $name('electronic_menu') . ' WHERE CREATE_BY=? OR UUID=? OR PUBLIC_TOKEN=?');
$q->execute([$marker, $menuUuid, $token]);
$menus = $q->fetchAll(PDO::FETCH_ASSOC);
$q = $db->prepare('SELECT ID,MENU_ID,CODE_HASH,CREATE_BY,STORE_ID FROM '
    . $name('electronic_menu_bind_code') . ' WHERE CREATE_BY=? OR CODE_HASH=?');
$q->execute([$marker, $codeHash]);
$codes = $q->fetchAll(PDO::FETCH_ASSOC);
$menuId = count($menus) === 1 ? (int)$menus[0]['id'] : 0;
$q = $db->prepare('SELECT COUNT(*) FROM ' . $name('electronic_menu_device')
    . ' WHERE DEVICE_ID IN (?,?) OR MENU_ID=?');
$q->execute([$journal['device_ids'][1] ?? '',
    $journal['device_ids'][2] ?? '', $menuId]);
$deviceCount = (int)$q->fetchColumn();
$q = $db->prepare('SELECT COUNT(*) FROM ' . $name('electronic_menu_device_log')
    . ' WHERE MENU_ID=? OR CREATE_BY LIKE ?');
$q->execute([$menuId, 'device:' . $marker . '%']);
$logCount = (int)$q->fetchColumn();
$state = json_decode((string)@file_get_contents(dirname($journalPath)
    . '/state.json'), true);
$routerPristine = is_array($state)
    && empty($state['arrivals'][1]) && empty($state['arrivals'][2])
    && ($state['released'] ?? null) === false
    && ($state['aborted'] ?? null) === false;
$baselineEqual = false;
if ($menus === [] && $codes === [] && $deviceCount === 0 && $logCount === 0) {
    $baselineEqual = true;
    foreach ($journal['source_before'] as $short => $sha) {
        $actual = hash('sha256', serialize($db->query('SELECT * FROM '
            . $name($short) . ' ORDER BY ID')->fetchAll(PDO::FETCH_ASSOC)));
        $baselineEqual = $baselineEqual && is_string($sha)
            && hash_equals($sha, $actual);
    }
}
$seen = ['menus' => $menus, 'codes' => $codes,
    'device_count' => $deviceCount, 'log_count' => $logCount,
    'router_pristine' => $routerPristine,
    // Pre-HTTP intent precedes curl dispatch; post-HTTP needs the journaled
    // completion barrier and zero rows. Unknown two_http_started is denied.
    'workers_quiescent' => $routerPristine
        || ($journal['phase'] ?? null) === 'both_worker_barriers_confirmed',
    'baseline_equal' => $baselineEqual];
$decision = raceWriteRecoveryDecision($journal, $seen);
if (($decision['allowed'] ?? false) !== true) {
    throw new RuntimeException('Exact write recovery denied: '
        . (string)($decision['reason'] ?? 'unknown'));
}
$write = static function (array $change) use (&$journal, $journalPath): void {
    $journal = array_replace_recursive($journal, $change);
    $text = json_encode($journal, JSON_UNESCAPED_SLASHES);
    if (!is_string($text) || file_put_contents($journalPath, $text, LOCK_EX) === false
        || !chmod($journalPath, 0600)) {
        throw new RuntimeException('Private recovery journal update failed');
    }
};
if ($decision['action'] === 'cleanup_pre_http') {
    // Durable recovered IDs first: failure here means no DELETE was attempted.
    $write(['menu_id' => $menuId,
        'code_id' => count($codes) === 1 ? (int)$codes[0]['id'] : 0]);
    $stage = 'exact_pre_http_transaction_cleanup';
    $db->beginTransaction();
    try {
        if ($codes !== []) {
            $q = $db->prepare('DELETE FROM ' . $name('electronic_menu_bind_code')
                . ' WHERE ID=? AND MENU_ID=? AND CODE_HASH=? AND CREATE_BY=?'
                . ' AND STORE_ID=10001');
            $q->execute([(int)$codes[0]['id'], $menuId, $codeHash, $marker]);
            if ($q->rowCount() !== 1) throw new RuntimeException('Exact code delete uncertain');
        }
        $q = $db->prepare('DELETE FROM ' . $name('electronic_menu')
            . ' WHERE ID=? AND UUID=? AND PUBLIC_TOKEN=? AND CREATE_BY=?'
            . ' AND STORE_ID=10001');
        $q->execute([$menuId, $menuUuid, $token, $marker]);
        if ($q->rowCount() !== 1) throw new RuntimeException('Exact menu delete uncertain');
        if (!$db->commit()) throw new RuntimeException('Exact cleanup commit uncertain');
    } catch (Throwable $error) {
        if ($db->inTransaction()) $db->rollBack();
        throw $error;
    }
}
$stage = 'separate_zero_audit_required';
$phase = $journal['phase'] === 'both_worker_barriers_confirmed'
    ? 'cleanup_committed' : 'recovery_cleanup_committed';
$write(['phase' => $phase]);
echo json_encode(['status' => 'exact_recovery_pending_independent_audit',
    'lane' => $lane, 'action' => $decision['action'],
    'journal_phase' => $phase, 'journal' => $journalPath,
    'business_http_sent' => false,
    'independent_zero_required_before_stop' => true],
    JSON_UNESCAPED_SLASHES), PHP_EOL;
