<?php
declare(strict_types=1);

// One self-owned completed-failure export-log row; never calls export or cron.
$lane = (string)($argv[1] ?? '');
$mode = (string)($argv[2] ?? '');
$markers = [
    'B' => 'CDXEX2749B93CC19C1',
    'C' => 'CDXEX20FC021A39A1C',
];
if (!isset($markers[$lane])
    || !in_array($mode, ['--preflight', '--execute', '--cleanup', '--audit'], true)
    || (in_array($mode, ['--execute', '--cleanup'], true)
        && ((string)getenv('CDX_EX2_APPROVE') !== 'ROOT_APPROVED_EXPORTLOGS_FAILED_ROW_V1'
            || !hash_equals(hash_file('sha256', __FILE__),
                (string)getenv('CDX_EX2_SHA'))))) {
    fwrite(STDERR, "EX2 default deny\n");
    exit(78);
}
$marker = $markers[$lane];
$root = $lane === 'B' ? '/tmp/zhct-http-store-mysql.dQTTBP'
    : '/tmp/zhct-dm-poc-20260928.fcpiiN/store';
$base = $lane === 'B' ? 'http://127.0.0.1:18081' : 'http://127.0.0.1:18080';
$pid = $lane === 'B' ? 10001 : 112175;
$journalPath = '/tmp/cdx-cashrefund-exportlogs-' . $lane . '-' . $marker . '.journal.json';
$sourceHashes = [
    '/application/route.php' => '081b0186b7f40f0bdd237cecb3e2d678fc9344c656932c0d3b3052404e4f1510',
    '/application/p/controller/CashRefund.php' => 'cd24ac67287603d536510ef7405b7fa3998c6bc99bc5a29399601cad0ea01172',
    '/application/cron/command/ExportMealOrder.php' => 'ed6dccdd2ff3b918ca0e283ecb74da4c4ff0a3b009df301f79b9bce367a82c0f',
    '/application/common/service/XlsxStreamWriter.php' => '1b2a67845fe13ba1cf4f19e32f65e3249a572c0022e34e9141a67186f0e4d759',
    '/application/base/AdminController.php' => '7565b36fa4dda2c21cc4fa3b37753dbffb7c3a464bb8c11ea0f17ca00bececa5',
];
$stage = 'boundary';
try {
    $runtimeReady = static function () use ($pid, $root, $base): bool {
        $cmd = (string)@file_get_contents('/proc/' . $pid . '/cmdline');
        $children = trim((string)@file_get_contents('/proc/' . $pid . '/task/' . $pid . '/children'));
        $boundary = json_decode((string)@file_get_contents($base . '/__poc_boundary'), true);
        return strpos($cmd, $root . '/public') !== false
            && strpos($cmd, (string)parse_url($base, PHP_URL_PORT)) !== false
            && $children === '' && is_array($boundary)
            && ($boundary['external_stubs'] ?? null) === true
            && ($boundary['redis_isolated'] ?? null) === true
            && ($boundary['real_queue'] ?? true) === false;
    };
    if (PHP_VERSION_ID < 70300 || PHP_VERSION_ID >= 70400
        || is_link($root) || !$runtimeReady()) {
        throw new RuntimeException('worker boundary');
    }
    foreach ($sourceHashes as $file => $sha) {
        if (!is_file($root . $file) || is_link($root . $file)
            || !hash_equals($sha, hash_file('sha256', $root . $file))) {
            throw new RuntimeException('source drift');
        }
    }
    $cron = (string)file_get_contents($root . '/application/cron/command/ExportMealOrder.php');
    if (strpos($cron, "->where('status', '=', 0)") === false
        || strpos($cron, "->where('type', 'in', [1,2,3,4,5,6,7,8,9,11") === false) {
        throw new RuntimeException('cron claim rule drift');
    }
    $stage = 'database';
    $cfg = require $root . '/application/database.php';
    if (!is_array($cfg) || ($cfg['prefix'] ?? '') !== 'ydy_') {
        throw new RuntimeException('database config');
    }
    if ($lane === 'B') {
        $local = require '/workspace/zhct/zhctproject/store/application/config/local/database.php';
        foreach (['hostname', 'database', 'username', 'password'] as $key) {
            if (($cfg[$key] ?? null) !== ($local[$key] ?? null)) {
                throw new RuntimeException('not code-local MySQL');
            }
        }
        $dsn = 'mysql:host=' . $cfg['hostname'] . ';port=' . ($cfg['hostport'] ?? 3306)
            . ';dbname=' . $cfg['database'] . ';charset=utf8mb4';
    } else {
        foreach (['hostname' => 'POC_HOST', 'hostport' => 'POC_PORT',
            'database' => 'POC_SCHEMA', 'username' => 'POC_USER',
            'password' => 'POC_PASSWORD'] as $key => $env) {
            if ((string)($cfg[$key] ?? '') !== (string)getenv($env)) {
                throw new RuntimeException('DM worker config');
            }
        }
        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 DM');
        }
        $dsn = 'dm:host=127.0.0.1;port=5237;schema=ZHCT_APP';
    }
    $db = new PDO($dsn, (string)$cfg['username'], (string)$cfg['password'], [
        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
        PDO::ATTR_CASE => PDO::CASE_LOWER, PDO::ATTR_TIMEOUT => 5,
    ]);
    $identity = $lane === 'B'
        ? (string)$db->query("SELECT CONCAT(@@server_uuid,'|',@@hostname,'|',@@port,'|',DATABASE())")->fetchColumn()
        : 'ZHCT_APP|5237|' . (string)$db->query("SELECT SYS_CONTEXT('USERENV','CURRENT_SCHEMA') FROM DUAL")->fetchColumn();
    if (hash('sha256', $identity) !== ($lane === 'B'
            ? '003eb5aecf1cd4595b609c1f7f20b9ee325b578309fb9fe14fedaf77584fa401'
            : 'bc98485aed26beab348770a82ae38dbf922b04ab88a210cd7282c4caf0156194')) {
        throw new RuntimeException('DB identity');
    }
    $p = $lane === 'B' ? 'ydy_' : 'YDY_';
    $read = static function (string $sql, array $args = [], int $cap = 100) use ($db): array {
        if (!preg_match('/^SELECT\b/i', $sql)) throw new RuntimeException('SELECT only');
        $stmt = $db->prepare($sql);
        $stmt->execute($args);
        $rows = [];
        while (($row = $stmt->fetch(PDO::FETCH_ASSOC)) !== false) {
            if (count($rows) >= $cap) throw new RuntimeException('read cap');
            $row = array_change_key_case($row, CASE_LOWER);
            foreach ($row as $key => $value) {
                if (is_resource($value)) $row[$key] = stream_get_contents($value);
            }
            ksort($row);
            $rows[] = $row;
        }
        return $rows;
    };
    $count = static function (string $sql, array $args = []) use ($read): int {
        $rows = $read($sql, $args, 1);
        return $rows ? (int)reset($rows[0]) : 0;
    };
    $write = static function (string $sql, array $args) use ($db): int {
        if (!preg_match('/^(INSERT|DELETE)\b/i', $sql)) {
            throw new RuntimeException('INSERT/DELETE only');
        }
        $stmt = $db->prepare($sql);
        $stmt->execute($args);
        return $stmt->rowCount();
    };
    $own = static function () use ($read, $p, $marker): array {
        $actor = $read('SELECT ID,UUID,ACCOUNT,NAME,TYPE,CREATE_BY FROM '
            . $p . 'user WHERE UUID=? OR ACCOUNT=?', [$marker, $marker], 3);
        $log = $read('SELECT ID,STORE_ID,TYPE,STATUS,FILE_NAME,FILE_PATH,'
            . ' CREATE_BY,CREATE_BY_UUID,CREATE_TIME,QUERY_CONDITIONS,HASH_KEY FROM '
            . $p . 'export_log WHERE CREATE_BY_UUID=? AND TYPE=11', [$marker], 3);
        return compact('actor', 'log');
    };
    $natural = static function () use ($read, $p, $marker): array {
        return $read('SELECT * FROM ' . $p . 'export_log'
            . ' WHERE STORE_ID=10001 AND TYPE=11 AND CREATE_BY_UUID<>? ORDER BY ID',
            [$marker], 500);
    };
    $digest = static function (array $rows): array {
        return ['count' => count($rows),
            'sha256' => hash('sha256', json_encode($rows,
                JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE))];
    };
    $journal = ['lane' => $lane, 'marker' => $marker,
        'identity_sha256' => hash('sha256', $identity),
        'phase' => 'before-fixture'];
    $saveJournal = static function (array $record) use ($journalPath): void {
        if (!is_file($journalPath) || is_link($journalPath)) {
            throw new RuntimeException('journal missing/symlink');
        }
        $json = json_encode($record, JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE);
        if (!is_string($json)) throw new RuntimeException('journal encode');
        $tmp = $journalPath . '.tmp.' . bin2hex(random_bytes(4));
        $h = fopen($tmp, 'x');
        if (!is_resource($h)) throw new RuntimeException('journal temp open');
        chmod($tmp, 0600);
        $n = fwrite($h, $json);
        fflush($h);
        fclose($h);
        if ($n !== strlen($json) || !rename($tmp, $journalPath)) {
            throw new RuntimeException('journal write');
        }
    };
    $loadJournal = static function () use ($journalPath, $lane, $marker, $identity): array {
        if (!is_file($journalPath) || is_link($journalPath)) {
            throw new RuntimeException('journal missing/symlink');
        }
        $v = json_decode((string)file_get_contents($journalPath), true);
        if (!is_array($v) || ($v['lane'] ?? null) !== $lane
            || ($v['marker'] ?? null) !== $marker
            || ($v['identity_sha256'] ?? null) !== hash('sha256', $identity)) {
            throw new RuntimeException('journal identity');
        }
        return $v;
    };
    $state = $own();
    if ($mode === '--audit') {
        $j = $loadJournal();
        $zero = !$state['actor'] && !$state['log'];
        $source = $digest($natural());
        $sourceUnchanged = $source === ($j['natural_before'] ?? null);
        echo json_encode(['lane' => $lane, 'marker' => $marker,
            'phase' => $j['phase'] ?? '', 'actor_zero' => !$state['actor'],
            'log_zero' => !$state['log'], 'natural_source_unchanged' => $sourceUnchanged]), PHP_EOL;
        exit($zero && $sourceUnchanged && ($j['phase'] ?? '') === 'verified-clean' ? 0 : 3);
    }
    if ($mode === '--preflight' || $mode === '--execute') {
        if (file_exists($journalPath) || is_link($journalPath)
            || $state['actor'] || $state['log']) {
            throw new RuntimeException('marker/journal collision');
        }
        $source = $digest($natural());
        $preflight = ['lane' => $lane, 'marker' => $marker,
            'own_counts' => ['actor' => 0, 'export_log' => 0],
            'natural_type11_count' => $source['count'],
            'natural_type11_sha12' => substr($source['sha256'], 0, 12),
            'cron_claim_only_status0' => true,
            'fixture_status5_not_claimed' => true,
            'journal_absent' => true];
        if ($mode === '--preflight') {
            echo json_encode($preflight), PHP_EOL;
            exit(0);
        }
    }
    $assertOwned = static function (array $s) use ($marker): void {
        if (count($s['actor']) > 1 || count($s['log']) > 1) {
            throw new RuntimeException('own row cap');
        }
        foreach ($s['actor'] as $actor) {
            if ($actor['uuid'] !== $marker || $actor['account'] !== $marker
                || $actor['name'] !== $marker || (int)$actor['type'] !== 0
                || $actor['create_by'] !== 'CODEX') {
                throw new RuntimeException('actor owner');
            }
        }
        foreach ($s['log'] as $log) {
            if ((int)$log['store_id'] !== 10001 || (int)$log['type'] !== 11
                || (int)$log['status'] !== 5
                || $log['file_name'] !== '' || $log['file_path'] !== ''
                || $log['create_by'] !== $marker
                || $log['create_by_uuid'] !== $marker
                || $log['query_conditions'] !== '{}'
                || $log['hash_key'] !== md5($marker)) {
                throw new RuntimeException('export-log owner/status');
            }
        }
    };
    $cleanup = static function (array &$record) use ($db, $own, $natural,
        $digest, $assertOwned, $saveJournal, $write, $p, $marker): void {
        $s = $own();
        $assertOwned($s);
        if ($digest($natural()) !== ($record['natural_before'] ?? null)) {
            throw new RuntimeException('natural source changed');
        }
        $record['phase'] = 'cleanup-start';
        $record['counts_before_cleanup'] = array_map('count', $s);
        $saveJournal($record);
        if (!$db->beginTransaction()) throw new RuntimeException('cleanup begin');
        try {
            foreach ($s['log'] as $log) {
                if ($write('DELETE FROM ' . $p . 'export_log'
                    . ' WHERE ID=? AND STORE_ID=10001 AND TYPE=11 AND STATUS=5'
                    . ' AND CREATE_BY_UUID=? AND HASH_KEY=?',
                    [(int)$log['id'], $marker, md5($marker)]) !== 1) {
                    throw new RuntimeException('export-log delete affected');
                }
            }
            foreach ($s['actor'] as $actor) {
                if ($write('DELETE FROM ' . $p . 'user'
                    . ' WHERE ID=? AND UUID=? AND ACCOUNT=? AND TYPE=0 AND CREATE_BY=?',
                    [(int)$actor['id'], $marker, $marker, 'CODEX']) !== 1) {
                    throw new RuntimeException('actor delete affected');
                }
            }
            $after = $own();
            if ($after['actor'] || $after['log']) throw new RuntimeException('cleanup not zero');
            if (!$db->commit()) throw new RuntimeException('cleanup commit');
        } catch (Throwable $error) {
            $rolledBack = false;
            try {
                if ($db->inTransaction()) $rolledBack = $db->rollBack();
            } catch (Throwable $rollbackError) {
                $record['rollback_error_sha12'] = substr(hash('sha256',
                    $rollbackError->getMessage()), 0, 12);
            }
            $record['phase'] = $rolledBack ? 'cleanup-rolled-back' : 'cleanup-uncertain';
            $record['cleanup_failure_sha12'] = substr(hash('sha256',
                $error->getMessage()), 0, 12);
            $saveJournal($record);
            throw $error;
        }
        $record['phase'] = 'verified-clean';
        $record['counts_after_cleanup'] = array_map('count', $own());
        $record['natural_after'] = $digest($natural());
        $saveJournal($record);
        if ($record['natural_after'] !== $record['natural_before']) {
            throw new RuntimeException('natural source after cleanup');
        }
    };
    if ($mode === '--cleanup') {
        $journal = $loadJournal();
        $cleanup($journal);
        echo json_encode(['lane' => $lane, 'marker' => $marker,
            'phase' => $journal['phase'],
            'own_counts' => array_map('count', $own()),
            'natural_source_unchanged' => $journal['natural_after'] === $journal['natural_before']]), PHP_EOL;
        exit(0);
    }
    $journal['natural_before'] = $source;
    $initial = json_encode($journal, JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE);
    if (!is_string($initial)) throw new RuntimeException('journal encode');
    $h = fopen($journalPath, 'x');
    if (!is_resource($h)) throw new RuntimeException('journal create');
    chmod($journalPath, 0600);
    $n = fwrite($h, $initial);
    fflush($h);
    fclose($h);
    if ($n !== strlen($initial)) throw new RuntimeException('journal initialize');
    $tree = static function ($value) use (&$tree) {
        if (is_int($value) || is_float($value)) return 'number';
        if (!is_array($value)) return gettype($value);
        if ($value === []) return [];
        if (array_keys($value) === range(0, count($value) - 1)) return [$tree($value[0])];
        $out = [];
        foreach ($value as $key => $item) $out[(string)$key] = $tree($item);
        ksort($out);
        return $out;
    };
    $post = static function (string $token) use ($base, $runtimeReady): array {
        if (!$runtimeReady()) throw new RuntimeException('HTTP boundary');
        $context = stream_context_create(['http' => ['method' => 'POST',
            'header' => "Content-Type: application/x-www-form-urlencoded\r\ntoken: " . $token . "\r\n",
            'content' => http_build_query(['page' => 1, 'page_size' => 10],
                '', '&', PHP_QUERY_RFC3986),
            'timeout' => 20, 'ignore_errors' => true]]);
        $stream = @fopen($base . '/p/cashRefund/exportLogs', 'rb', false, $context);
        $body = is_resource($stream) ? stream_get_contents($stream, 2097153) : false;
        $meta = is_resource($stream) ? stream_get_meta_data($stream) : [];
        if (is_resource($stream)) fclose($stream);
        if (!is_string($body) || strlen($body) > 2097152 || !empty($meta['timed_out'])
            || strpos((string)($http_response_header[0] ?? ''), ' 200 ') === false) {
            throw new RuntimeException('HTTP incomplete');
        }
        $json = json_decode($body, true);
        if (!is_array($json) || ($json['code'] ?? null) !== 0
            || !is_array($json['data'] ?? null)) {
            throw new RuntimeException('HTTP response');
        }
        return $json;
    };
    $outcome = ['lane' => $lane, 'marker' => $marker,
        'status' => 'started', 'contract' => null,
        'source_unchanged' => false, 'cleaned' => false,
        'journal' => $journalPath];
    $failure = '';
    try {
        $stage = 'fixture-actor';
        if ($write('INSERT INTO ' . $p . 'user'
            . ' (UUID,ACCOUNT,PWD,SALT,NAME,TYPE,CREATE_TIME,CREATE_BY,IS_DELETE)'
            . ' VALUES (?,?,?,?,?,?,?,?,?)',
            [$marker, $marker, 'DISABLED', strtoupper(bin2hex(random_bytes(8))),
                $marker, 0, date('Y-m-d H:i:s'), 'CODEX', 0]) !== 1) {
            throw new RuntimeException('actor insert');
        }
        $s = $own();
        $assertOwned($s);
        $journal['phase'] = $stage;
        $journal['actor_id'] = (int)$s['actor'][0]['id'];
        $saveJournal($journal);
        require_once $root . '/application/base/Security.php';
        $token = \app\base\Security::encrypt(implode("\t",
            [(string)$journal['actor_id'], $marker, $marker, '0', '', '', $marker]));
        if (!is_string($token) || $token === '') throw new RuntimeException('token');

        $stage = 'fixture-failed-export-log';
        if ($write('INSERT INTO ' . $p . 'export_log'
            . ' (STORE_ID,TYPE,STATUS,FILE_NAME,FILE_PATH,CREATE_BY,CREATE_BY_UUID,'
            . ' CREATE_TIME,QUERY_CONDITIONS,HASH_KEY) VALUES (?,?,?,?,?,?,?,?,?,?)',
            [10001, 11, 5, '', '', $marker, $marker,
                date('Y-m-d H:i:s'), '{}', md5($marker)]) !== 1) {
            throw new RuntimeException('failed export-log insert');
        }
        $s = $own();
        $assertOwned($s);
        $journal['phase'] = $stage;
        $journal['log_id'] = (int)$s['log'][0]['id'];
        $saveJournal($journal);
        if ($digest($natural()) !== $source) throw new RuntimeException('natural source changed');

        $stage = 'http-exportLogs';
        $response = $post($token);
        $s = $own();
        $assertOwned($s);
        $data = $response['data'];
        $rows = $data['data'] ?? null;
        if (!is_array($rows) || count($rows) !== 1
            || (int)($data['total'] ?? -1) !== 1
            || (int)($rows[0]['id'] ?? 0) !== (int)$s['log'][0]['id']
            || (string)($rows[0]['operator'] ?? '') !== $marker
            || (int)($rows[0]['status'] ?? -1) !== 5
            || (string)($rows[0]['status_text'] ?? '') !== '失败'
            || substr((string)($rows[0]['operate_time'] ?? ''), 0, 19)
                !== substr((string)$s['log'][0]['create_time'], 0, 19)
            || (string)($rows[0]['file_name'] ?? 'X') !== ''
            || (string)($rows[0]['url'] ?? 'X') !== '') {
            throw new RuntimeException('HTTP/original export-log mismatch');
        }
        $outcome['contract'] = $tree($response);
        $outcome['source_unchanged'] = $digest($natural()) === $source;
        if (!$outcome['source_unchanged']) throw new RuntimeException('natural source changed');
        $journal['phase'] = $stage;
        $journal['response_type_tree'] = $outcome['contract'];
        $saveJournal($journal);
        $outcome['status'] = 'business-pass-before-cleanup';
    } catch (Throwable $error) {
        $failure = $stage . ':' . get_class($error) . ':' . (string)$error->getCode()
            . ':' . substr(hash('sha256', $error->getMessage()), 0, 12);
        $outcome['status'] = 'business-fail';
        $outcome['failure'] = $failure;
    }
    try {
        $cleanup($journal);
        $outcome['cleaned'] = true;
    } catch (Throwable $cleanupError) {
        $outcome['cleanup_failure_sha12'] = substr(hash('sha256',
            $cleanupError->getMessage()), 0, 12);
        $outcome['status'] = 'cleanup-needs-attention';
    }
    echo json_encode($outcome, JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE), PHP_EOL;
    exit($failure === '' && $outcome['status'] === 'business-pass-before-cleanup'
        && $outcome['source_unchanged'] && $outcome['cleaned'] ? 0 : 1);
} catch (Throwable $error) {
    echo json_encode(['lane' => $lane, 'mode' => $mode,
        'status' => 'blocked', 'stage' => $stage,
        'class' => get_class($error), 'code' => (string)$error->getCode(),
        'message_sha12' => substr(hash('sha256', $error->getMessage()), 0, 12)]), PHP_EOL;
    exit(2);
}
