<?php
declare(strict_types=1);

// B/C nonproduction external-meal fixture and HTTP read/write batch.
$lane = (string)getenv('CDX_MEAL_LANE');
$mode = (string)($argv[1] ?? '');
$self = (string)hash_file('sha256', __FILE__);
if (!in_array($lane, ['B', 'C'], true)
    || count($argv) !== 2
    || !in_array($mode, ['--preflight', '--execute', '--audit'], true)
    || ($mode === '--execute'
        && ((string)getenv('CDX_MEAL_APPROVE') !== 'ROOT_OWNED_EXTERNAL_MEAL_V1'
            || !hash_equals($self, (string)getenv('CDX_MEAL_SHA'))))) {
    echo json_encode(['status' => 'default_deny', 'connected' => false,
        'http_sent' => false, 'dml' => false]), PHP_EOL;
    exit(78);
}
$stage = 'runtime';
set_exception_handler(static function (Throwable $error) use (&$stage): void {
    echo json_encode(['status' => 'redacted_stop', 'stage' => $stage,
        'class' => get_class($error), 'code' => (string)$error->getCode(),
        'recovery' => 'inspect_private_journal']), PHP_EOL;
    exit(2);
});
$target = [
    'B' => ['/tmp/zhct-http-store-mysql.dQTTBP', 10001, 18081, 'mysql'],
    'C' => ['/tmp/zhct-dm-poc-20260928.fcpiiN/store', 112175, 18080, 'dm'],
][$lane];
[$root, $pid, $port, $dialect] = $target;
if (PHP_VERSION_ID < 70300 || PHP_VERSION_ID >= 70400 || is_link($root)) {
    throw new RuntimeException('PHP/root drift');
}
$hashes = [
    '/application/p/controller/Meal.php' => 'fe64cbac5b5b6fc583026c70d765922deb9811150f9e0dced0b81aba0f9b5b7f',
    '/application/p/logic/Meal.php' => '332f8fceae55d846f9b49a83ad0ce151ede6d584511783c0a3f47dc06aed5722',
    '/application/model/StaffMeal.php' => '7b71ad5c500c63c362126fe08a81564b9cfc4fa42bf3600fb25129985d3b4a4e',
    '/application/model/MealStatistics.php' => 'decef495044c8429f66d122881dde923fae45690902310999c08e353d9514a38',
];
foreach ($hashes as $path => $sha) {
    if (hash_file('sha256', $root . $path) !== $sha) {
        throw new RuntimeException('Business source drift');
    }
}
$command = (string)@file_get_contents('/proc/' . $pid . '/cmdline');
$children = trim((string)@file_get_contents('/proc/' . $pid . '/task/' . $pid . '/children'));
if (strpos($command, $root . '/public') === false
    || strpos($command, '127.0.0.1:' . $port) === false || $children !== '') {
    throw new RuntimeException('Single-worker listener drift');
}
$base = 'http://127.0.0.1:' . $port;
$boundary = json_decode((string)@file_get_contents($base . '/__poc_boundary'), true);
if (!is_array($boundary) || ($boundary['external_stubs'] ?? null) !== true
    || ($boundary['redis_isolated'] ?? null) !== true
    || ($boundary['real_queue'] ?? true) !== false) {
    throw new RuntimeException('External/queue isolation absent');
}
$runtime = require $root . '/application/database.php';
if ($dialect === 'mysql') {
    $config = $runtime;
    $local = require '/workspace/zhct/zhctproject/store/application/config/local/database.php';
    unset($config);
    foreach (['hostname', 'hostport', 'database', 'username', 'password', 'type'] as $key) {
        if (($runtime[$key] ?? null) !== ($local[$key] ?? null)) {
            throw new RuntimeException('Not code-local development MySQL');
        }
    }
    $dsn = 'mysql:host=' . $runtime['hostname'] . ';port=' . (int)$runtime['hostport']
        . ';dbname=' . $runtime['database'] . ';charset=' . ($runtime['charset'] ?? 'utf8');
} else {
    foreach (['hostname' => 'POC_HOST', 'hostport' => 'POC_PORT',
        'database' => 'POC_SCHEMA', 'username' => 'POC_USER',
        'password' => 'POC_PASSWORD'] as $key => $env) {
        if ((string)($runtime[$key] ?? '') !== (string)getenv($env)) {
            throw new RuntimeException('C runtime/worker DB drift');
        }
    }
    if ((string)getenv('POC_HOST') !== '127.0.0.1'
        || (string)getenv('POC_PORT') !== '5237'
        || strtoupper((string)getenv('POC_SCHEMA')) !== 'ZHCT_APP') {
        throw new RuntimeException('Not approved nonproduction DM');
    }
    $dsn = 'dm:host=127.0.0.1;port=5237;schema=ZHCT_APP';
}
$connect = static function () use ($dsn, $runtime): PDO {
    return new PDO($dsn, (string)$runtime['username'], (string)$runtime['password'], [
        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
        PDO::ATTR_CASE => PDO::CASE_LOWER,
        PDO::ATTR_TIMEOUT => 5,
    ]);
};
$db = $connect();
if ($dialect === 'mysql') {
    $identity = (string)$db->query(
        "SELECT CONCAT(@@server_uuid,'|',@@hostname,'|',@@port,'|',DATABASE())")
        ->fetchColumn();
    if (hash('sha256', $identity)
        !== '003eb5aecf1cd4595b609c1f7f20b9ee325b578309fb9fe14fedaf77584fa401') {
        throw new RuntimeException('B DB identity drift');
    }
} elseif (strtoupper((string)$db->query(
    "SELECT SYS_CONTEXT('USERENV','CURRENT_SCHEMA') FROM DUAL")->fetchColumn())
    !== 'ZHCT_APP') {
    throw new RuntimeException('C schema drift');
}
$table = static function (string $name) use ($dialect): string {
    if (!in_array($name, ['user', 'staff_meal', 'meal_statistics'], true)) {
        throw new RuntimeException('Unapproved table');
    }
    return $dialect === 'dm' ? strtoupper('ydy_' . $name) : 'ydy_' . $name;
};
$day = '2099-01-01';
$meal = 2;
$count = static function (PDO $connection, string $name) use ($table, $day, $meal): int {
    $q = $connection->prepare('SELECT COUNT(*) FROM ' . $table($name)
        . ' WHERE DATE=? AND MEAL_TIMES=?');
    $q->execute([$day, $meal]);
    return (int)$q->fetchColumn();
};
$journalPath = (string)getenv('CDX_MEAL_JOURNAL');
if ($mode === '--audit') {
    if (!preg_match('~^/tmp/cdx-meal-ext-[a-f0-9]{12}/recovery\.json$~D', $journalPath)
        || is_link($journalPath) || is_link(dirname($journalPath))
        || (fileperms(dirname($journalPath)) & 0777) !== 0700
        || (fileperms($journalPath) & 0777) !== 0600) {
        throw new RuntimeException('Private audit journal invalid');
    }
    $saved = json_decode((string)file_get_contents($journalPath), true);
    if (!is_array($saved) || ($saved['lane'] ?? null) !== $lane
        || ($saved['phase'] ?? null) !== 'cleanup_complete'
        || !preg_match('/^CDXMEAL[A-F0-9]{12}$/D', (string)($saved['marker'] ?? ''))
        || (int)($saved['actor_id'] ?? 0) <= 0) {
        throw new RuntimeException('Journal ownership/phase invalid');
    }
    $actor = $db->prepare('SELECT COUNT(*) FROM ' . $table('user')
        . ' WHERE ID=? OR UUID=? OR ACCOUNT=?');
    $actor->execute([(int)$saved['actor_id'], $saved['marker'], $saved['marker']]);
    $owned = $db->prepare('SELECT COUNT(*) FROM ' . $table('staff_meal')
        . ' WHERE CREATE_BY=? OR DEPARTMENT_NAME=?');
    $owned->execute([$saved['marker'], $saved['marker']]);
    if ((int)$actor->fetchColumn() !== 0 || (int)$owned->fetchColumn() !== 0
        || $count($db, 'staff_meal') !== 0
        || $count($db, 'meal_statistics') !== 0) {
        throw new RuntimeException('Independent owned zero failed');
    }
    unlink($journalPath);
    rmdir(dirname($journalPath));
    echo json_encode(['status' => 'independent_zero_pass', 'lane' => $lane,
        'owned_actor' => 0, 'staff_meal' => 0, 'meal_statistics' => 0,
        'private_journal_removed' => true]), PHP_EOL;
    exit(0);
}
$stage = 'source_preflight';
$oldActors = (int)$db->query('SELECT COUNT(*) FROM ' . $table('user')
    . " WHERE ACCOUNT LIKE 'CDXMEAL%'")->fetchColumn();
$staffBefore = $count($db, 'staff_meal');
$statsBefore = $count($db, 'meal_statistics');
if ($mode === '--preflight') {
    echo json_encode(['status' => 'select_only_ready', 'lane' => $lane,
        'future_day_rows' => ['staff_meal' => $staffBefore,
            'meal_statistics' => $statsBefore],
        'old_actor_count' => $oldActors, 'http_sent' => false, 'dml' => false]), PHP_EOL;
    exit(0);
}
if ($oldActors !== 0 || $staffBefore !== 0 || $statsBefore !== 0) {
    throw new RuntimeException('Future fixture scope not empty');
}
$marker = 'CDXMEAL' . strtoupper(bin2hex(random_bytes(6)));
$directory = '/tmp/cdx-meal-ext-' . strtolower(bin2hex(random_bytes(6)));
if (file_exists($directory) || !mkdir($directory, 0700)) {
    throw new RuntimeException('Private recovery directory unavailable');
}
chmod($directory, 0700);
$journalPath = $directory . '/recovery.json';
$saved = ['lane' => $lane, 'marker' => $marker,
    'actor_id' => 0, 'phase' => 'before_actor_insert'];
$save = static function () use (&$saved, $journalPath): void {
    if (file_put_contents($journalPath, json_encode($saved), LOCK_EX) === false
        || !chmod($journalPath, 0600)) {
        throw new RuntimeException('Recovery journal write failed');
    }
};
$save();
$result = ['status' => 'FAIL', 'lane' => $lane];
$failure = '';
try {
    $stage = 'actor_insert';
    $db->prepare('INSERT INTO ' . $table('user')
        . ' (UUID,ACCOUNT,PWD,SALT,NAME,TYPE,CREATE_TIME,CREATE_BY,IS_DELETE)'
        . ' VALUES (?,?,?,?,?,0,?,?,0)')->execute([
            $marker, $marker, 'DISABLED', strtoupper(bin2hex(random_bytes(8))),
            $marker, date('Y-m-d H:i:s'), 'CODEX',
        ]);
    $q = $db->prepare('SELECT ID FROM ' . $table('user')
        . ' WHERE UUID=? AND ACCOUNT=? AND TYPE=0 AND CREATE_BY=?');
    $q->execute([$marker, $marker, 'CODEX']);
    $ids = $q->fetchAll(PDO::FETCH_COLUMN);
    if (count($ids) !== 1 || (int)$ids[0] <= 0) {
        throw new RuntimeException('Actor ownership ambiguous');
    }
    $saved['actor_id'] = (int)$ids[0];
    $saved['phase'] = 'actor_inserted'; $save();
    require_once $root . '/application/base/Security.php';
    $token = \app\base\Security::encrypt(implode("\t", [
        (string)$saved['actor_id'], $marker, $marker, '0', '', '', $marker,
    ]));
    $request = static function (string $path, array $form) use ($base, $token): array {
        $stream = @fopen($base . $path, 'rb', false,
            stream_context_create(['http' => [
                'method' => 'POST', 'timeout' => 15, 'ignore_errors' => true,
                'header' => "Content-Type: application/x-www-form-urlencoded\r\ntoken: "
                    . $token . "\r\n",
                'content' => http_build_query($form, '', '&', PHP_QUERY_RFC3986),
            ]]));
        $body = $stream === false ? false : stream_get_contents($stream, 1048577);
        $headers = $http_response_header ?? [];
        if (is_resource($stream)) fclose($stream);
        preg_match('/HTTP\/\S+\s+(\d+)/', (string)($headers[0] ?? ''), $match);
        return [(int)($match[1] ?? 0), is_string($body) ? json_decode($body, true) : null];
    };
    $common = ['start_date' => $day, 'end_date' => $day,
        'source_type' => 2, 'meal_times' => (string)$meal,
        'people_number' => 1, 'department_name' => $marker,
        'staff_name' => 'CDXTEST'];
    $stage = 'enter_http';
    $saved['phase'] = 'enter_http_started'; $save();
    [$http, $body] = $request('/p/meal/add', $common + ['retreat_type' => 1]);
    if ($http !== 200 || !is_array($body) || ($body['code'] ?? null) !== 0) {
        throw new RuntimeException('External enter HTTP failed');
    }
    $rows = $db->prepare('SELECT ID,UUID,DEPARTMENT_UUID,STAFF_UUID,PEOPLE_NUMBER,CREATE_TIME'
        . ' FROM ' . $table('staff_meal')
        . ' WHERE CREATE_BY=? AND DEPARTMENT_NAME=? AND DATE=? AND MEAL_TIMES=?'
        . ' AND SOURCE_TYPE=2 AND RETREAT_TYPE=1');
    $rows->execute([$marker, $marker, $day, $meal]);
    $entered = $rows->fetchAll(PDO::FETCH_ASSOC);
    if (count($entered) !== 1 || (string)$entered[0]['department_uuid'] !== ''
        || (string)$entered[0]['staff_uuid'] !== ''
        || (int)$entered[0]['people_number'] !== 1
        || $count($db, 'meal_statistics') !== 1) {
        throw new RuntimeException('External enter original rows mismatch');
    }
    $result['enter_write_original_rows'] = true;
    $stage = 'read_http';
    $reads = [
        'lists' => ['/p/meal/lists', ['time' => '2099-01']],
        'recordShow' => ['/p/meal/recordShow', ['time' => $day]],
        'recordSee' => ['/p/meal/recordSee',
            ['create_time' => (string)$entered[0]['create_time'], 'retreat_type' => 1]],
        'informationOverview' => ['/p/meal/informationOverview',
            ['start_time' => $day, 'end_time' => $day]],
        'singleEnter' => ['/p/meal/singleEnter',
            ['date' => $day, 'meal_times' => $meal]],
        'dateMealShow' => ['/p/meal/dateMealShow',
            ['date' => $day, 'meal_times' => $meal, 'retreat_type' => 1]],
        'manyEnter' => ['/p/meal/manyEnter',
            ['start_time' => $day, 'end_time' => $day, 'meal_times' => $meal]],
        'stayPartnership' => ['/p/meal/stayPartnership',
            ['start_time' => $day, 'end_time' => $day,
                'staff_uuid' => $marker]],
    ];
    $typeShape = static function ($node) use (&$typeShape) {
        if (!is_array($node)) return gettype($node);
        $shape = [];
        foreach ($node as $key => $value) $shape[$key] = $typeShape($value);
        if (array_keys($shape) !== range(0, count($shape) - 1)) ksort($shape);
        return $shape;
    };
    $seen = [];
    foreach ($reads as $name => [$path, $form]) {
        [$http, $body] = $request($path, $form);
        if ($http !== 200 || !is_array($body) || ($body['code'] ?? null) !== 0
            || !array_key_exists('data', $body)) {
            throw new RuntimeException('Read HTTP failed at ' . $name);
        }
        $data = $body['data'];
        $sourceMatch = false;
        switch ($name) {
            case 'lists':
                $sourceMatch = isset($data[0])
                    && substr((string)($data[0]['date'] ?? ''), 0, 10) === $day
                    && (int)($data[0]['quantity2'] ?? -1) === 1;
                break;
            case 'recordShow':
                $sourceMatch = isset($data[0])
                    && (string)($data[0]['department_name'] ?? '') === $marker
                    && (int)($data[0]['source_type'] ?? -1) === 2
                    && (int)($data[0]['people_number'] ?? -1) === 1;
                break;
            case 'recordSee':
                $sourceMatch = (string)($data['department_name'] ?? '') === $marker
                    && substr((string)($data['start_time'] ?? ''), 0, 10) === $day
                    && substr((string)($data['end_time'] ?? ''), 0, 10) === $day
                    && in_array('CDXTEST', (array)($data['staff_name'] ?? []), true);
                break;
            case 'informationOverview':
                $sourceMatch = isset($data['rows'][0])
                    && substr((string)($data['rows'][0]['date'] ?? ''), 0, 10) === $day
                    && (int)($data['rows'][0]['quantity2'] ?? -1) === 1
                    && (int)($data['total'] ?? -1) === 1;
                break;
            case 'singleEnter':
                $sourceMatch = isset($data['table'][0], $data['columnar'][0])
                    && (string)($data['table'][0]['name'] ?? '') === '外来人员'
                    && (int)($data['table'][0]['none'] ?? -1) === 1
                    && (int)($data['columnar'][0]['enter_count'] ?? -1) === 1;
                break;
            case 'dateMealShow':
                $sourceMatch = isset($data[0])
                    && (string)($data[0]['department_name'] ?? '') === $marker
                    && (int)($data[0]['people_number'] ?? -1) === 1;
                break;
            case 'manyEnter':
                $sourceMatch = ($data['table'] ?? null) === []
                    && ($data['columnar'] ?? null) === [];
                break;
            case 'stayPartnership':
                $sourceMatch = $data === [];
                break;
        }
        $seen[$name] = ['nonempty' => is_array($data) && $data !== [],
            'source_match' => $sourceMatch,
            'type_sha' => hash('sha256', serialize($typeShape($data)))];
        if (!$sourceMatch) throw new RuntimeException('Read/source mismatch at ' . $name);
    }
    $result['reads'] = $seen;
    $stage = 'retreat_http';
    $saved['phase'] = 'retreat_http_started'; $save();
    [$http, $body] = $request('/p/meal/add', $common + ['retreat_type' => 2]);
    if ($http !== 200 || !is_array($body) || ($body['code'] ?? null) !== 0) {
        throw new RuntimeException('External retreat HTTP failed');
    }
    if ($count($db, 'staff_meal') !== 2
        || $count($db, 'meal_statistics') !== 1) {
        throw new RuntimeException('External retreat row counts mismatch');
    }
    $stats = $db->prepare('SELECT QUANTITY FROM ' . $table('meal_statistics')
        . ' WHERE DATE=? AND MEAL_TIMES=?');
    $stats->execute([$day, $meal]);
    if ((int)$stats->fetchColumn() !== 0) {
        throw new RuntimeException('External retreat quantity not zero');
    }
    $result['retreat_write_original_rows'] = true;
    $result['status'] = 'HTTP_PASS_PENDING_INDEPENDENT_AUDIT';
} catch (Throwable $error) {
    $failure = get_class($error) . ':' . (string)$error->getCode();
}
$stage = 'completion_barrier';
$afterCmd = (string)@file_get_contents('/proc/' . $pid . '/cmdline');
$afterBoundary = json_decode((string)@file_get_contents($base . '/__poc_boundary'), true);
if (strpos($afterCmd, $root . '/public') === false
    || trim((string)@file_get_contents('/proc/' . $pid . '/task/' . $pid . '/children')) !== ''
    || !is_array($afterBoundary)
    || ($afterBoundary['external_stubs'] ?? null) !== true
    || ($afterBoundary['real_queue'] ?? true) !== false) {
    echo json_encode(['status' => 'uncertain_http_retain_journal',
        'lane' => $lane, 'journal' => $journalPath]), PHP_EOL;
    exit(3);
}
$stage = 'exact_cleanup';
$clean = $connect();
$owned = $clean->prepare('SELECT ID,UUID,CREATE_BY,DEPARTMENT_NAME,SOURCE_TYPE'
    . ' FROM ' . $table('staff_meal') . ' WHERE DATE=? AND MEAL_TIMES=?');
$owned->execute([$day, $meal]);
$ownedRows = $owned->fetchAll(PDO::FETCH_ASSOC);
if (count($ownedRows) > 2) throw new RuntimeException('Unknown future rows; retain journal');
foreach ($ownedRows as $row) {
    if ((string)$row['create_by'] !== $marker
        || (string)$row['department_name'] !== $marker
        || (int)$row['source_type'] !== 2) {
        throw new RuntimeException('Foreign future row; retain journal');
    }
}
$stat = $clean->prepare('SELECT ID,QUANTITY FROM ' . $table('meal_statistics')
    . ' WHERE DATE=? AND MEAL_TIMES=?');
$stat->execute([$day, $meal]);
$statsRows = $stat->fetchAll(PDO::FETCH_ASSOC);
if (count($statsRows) > 1) throw new RuntimeException('Unknown stats rows; retain journal');
if ($statsRows !== [] && ($ownedRows === []
    || !in_array((int)$statsRows[0]['quantity'], [0, 1], true))) {
    throw new RuntimeException('Statistics ownership/quantity uncertain');
}
$clean->beginTransaction();
foreach ($ownedRows as $row) {
    $del = $clean->prepare('DELETE FROM ' . $table('staff_meal')
        . ' WHERE ID=? AND UUID=? AND CREATE_BY=? AND DEPARTMENT_NAME=?'
        . ' AND DATE=? AND MEAL_TIMES=? AND SOURCE_TYPE=2');
    $del->execute([(int)$row['id'], $row['uuid'], $marker, $marker, $day, $meal]);
    if ($del->rowCount() !== 1) throw new RuntimeException('Owned meal delete mismatch');
}
foreach ($statsRows as $row) {
    $del = $clean->prepare('DELETE FROM ' . $table('meal_statistics')
        . ' WHERE ID=? AND DATE=? AND MEAL_TIMES=? AND QUANTITY=?');
    $del->execute([(int)$row['id'], $day, $meal, (int)$row['quantity']]);
    if ($del->rowCount() !== 1) throw new RuntimeException('Owned stats delete mismatch');
}
$delActor = $clean->prepare('DELETE FROM ' . $table('user')
    . ' WHERE ID=? AND UUID=? AND ACCOUNT=? AND TYPE=0 AND CREATE_BY=?');
$delActor->execute([(int)$saved['actor_id'], $marker, $marker, 'CODEX']);
if ((int)$saved['actor_id'] > 0 && $delActor->rowCount() !== 1) {
    throw new RuntimeException('Owned actor delete mismatch');
}
if (!$clean->commit()) throw new RuntimeException('Cleanup commit uncertain');
$saved['phase'] = 'cleanup_complete'; $save();
$result['cleanup'] = ['staff_meal_deleted' => count($ownedRows),
    'statistics_deleted' => count($statsRows), 'actor_deleted' => (int)$saved['actor_id'] > 0];
$result['journal'] = $journalPath;
if ($failure !== '') $result['failure'] = $failure;
echo json_encode($result, JSON_UNESCAPED_SLASHES), PHP_EOL;
exit($failure === '' ? 0 : 2);
