<?php

declare(strict_types=1);

$dialect = (string)getenv('POC_DIALECT');
$root = (string)getenv('POC_APP_ROOT');
$base = rtrim((string)getenv('POC_BASE_URL'), '/');
$account = (string)getenv('POC_MEAL_ACCOUNT');
$stub = (string)getenv('POC_OSS_STUB');
if (!in_array($dialect, ['mysql', 'dm'], true) || strpos($root, '/tmp/zhct-') !== 0
    || $base === '' || !preg_match('/^CDXPAY[A-F0-9]{8}$/D', $account)
    || !is_file($stub) || !is_file($root . '/application/base/Security.php')) {
    throw new RuntimeException('Meal export isolated settings invalid');
}
$boundary = json_decode((string)file_get_contents($base . '/__poc_boundary'), true);
if (!is_array($boundary) || empty($boundary['external_stubs'])
    || empty($boundary['redis_isolated'])) {
    throw new RuntimeException('Meal export isolation boundary absent');
}
$downloadRoot = $root . '/public/static/download';
$dateDir = $downloadRoot . '/' . date('Ymd');
$rootExisted = is_dir($downloadRoot);
if (is_link($downloadRoot) || is_link($dateDir) || is_dir($dateDir)
    || (file_exists($downloadRoot) && realpath($downloadRoot) !== $downloadRoot)) {
    throw new RuntimeException('Meal export directory is not exclusive to this run');
}

$dbConfig = [];
foreach (['POC_HOST', 'POC_PORT', 'POC_SCHEMA', 'POC_USER', 'POC_PASSWORD'] as $key) {
    $dbConfig[$key] = (string)getenv($key);
}
$runtimePid = (string)getenv('POC_RUNTIME_PID');
if ($dialect === 'dm' && $dbConfig['POC_HOST'] === '' && ctype_digit($runtimePid)) {
    foreach (explode("\0", (string)file_get_contents('/proc/' . $runtimePid . '/environ')) as $entry) {
        $parts = explode('=', $entry, 2);
        if (count($parts) === 2 && array_key_exists($parts[0], $dbConfig)) {
            $dbConfig[$parts[0]] = $parts[1];
        }
    }
}
if ($dialect === 'mysql' && $dbConfig['POC_HOST'] === '') {
    $local = $root . '/application/config/local/database.php';
    $config = require (is_file($local) ? $local : $root . '/application/database.php');
    $dbConfig = [
        'POC_HOST' => (string)$config['hostname'],
        'POC_PORT' => (string)($config['hostport'] ?? 3306),
        'POC_SCHEMA' => (string)$config['database'],
        'POC_USER' => (string)$config['username'],
        'POC_PASSWORD' => (string)$config['password'],
    ];
}
if ($dbConfig['POC_HOST'] === '' || $dbConfig['POC_SCHEMA'] === ''
    || $dbConfig['POC_USER'] === '') throw new RuntimeException('Store DB unavailable');
$dsn = $dialect === 'dm'
    ? "dm:host={$dbConfig['POC_HOST']};port={$dbConfig['POC_PORT']};schema={$dbConfig['POC_SCHEMA']}"
    : "mysql:host={$dbConfig['POC_HOST']};port={$dbConfig['POC_PORT']};dbname={$dbConfig['POC_SCHEMA']};charset=utf8mb4";
$db = new PDO($dsn, $dbConfig['POC_USER'], $dbConfig['POC_PASSWORD'],
    [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
$prefix = $dialect === 'dm' ? 'YDY_' : 'ydy_';
$table = static function (string $name) use ($prefix, $dialect): string {
    return $prefix . ($dialect === 'dm' ? strtoupper($name) : $name);
};
$dbString = static function ($value): string {
    return is_resource($value) ? (string)stream_get_contents($value) : (string)$value;
};
$read = static function (string $sql, array $params = []) use ($db): array {
    $query = $db->prepare($sql);
    $query->execute($params);
    return array_change_key_case($query->fetch(PDO::FETCH_ASSOC) ?: [], CASE_LOWER);
};
$pending = static function () use ($db, $table): int {
    return (int)$db->query('SELECT COUNT(*) FROM ' . $table('export_log') . ' WHERE STATUS=0')->fetchColumn();
};
if ($pending() !== 0) throw new RuntimeException('Other export jobs are pending');
$sourceQuery = $db->prepare('SELECT m.ID,m.ORDER_NO,m.USER_ID,m.BIND_TIME,m.MEAL_NUMBER,'
    . ' m.ORDER_PRICE,m.DISCOUNT_AMOUNT,m.MANAGEMENT_FEE,m.PAY_PRICE,'
    . ' m.CASH,m.SUBSIDY,m.GIFT,m.GIFT_REFUND_AMOUNT,m.REFUND_AMOUNT'
    . ' FROM ' . $table('meal_order') . ' m JOIN ' . $table('user') . ' u ON u.ID=m.USER_ID'
    . ' WHERE u.ACCOUNT=? AND u.NAME=? AND m.IS_DELETE=0 ORDER BY m.ID DESC '
    . ($dialect === 'dm' ? 'FETCH FIRST 2 ROWS ONLY' : 'LIMIT 2'));
$sourceQuery->execute([$account, $account]);
$sourceRows = array_map('array_change_key_case', $sourceQuery->fetchAll(PDO::FETCH_ASSOC));
if (count($sourceRows) !== 1) throw new RuntimeException('One CDXPAY meal order required');
$source = $sourceRows[0];
$bindDate = substr((string)$source['bind_time'], 0, 10);

require_once $root . '/application/base/Security.php';
$admin = 'CDXPARSTMEEXP' . strtoupper(bin2hex(random_bytes(6)));
$adminId = 0;
$jobId = 0;
$file = '';
$results = [];
$token = \app\base\Security::encrypt(implode("\t", ['0', $admin, $admin, '0', '', '', $admin]));
$post = static function (string $route, array $form, bool $auth = true) use ($base, $token): array {
    $headers = "Content-Type: application/x-www-form-urlencoded\r\n";
    if ($auth) $headers .= 'token: ' . $token . "\r\n";
    $context = stream_context_create(['http' => [
        'method' => 'POST', 'header' => $headers,
        'content' => http_build_query($form, '', '&', PHP_QUERY_RFC3986),
        'timeout' => 20, 'ignore_errors' => true,
    ]]);
    $body = file_get_contents($base . $route, false, $context);
    $headersOut = $http_response_header ?? [];
    if (strpos((string)($headersOut[0] ?? ''), ' 200 ') === false) {
        throw new RuntimeException('Meal export HTTP status mismatch');
    }
    $response = is_string($body) ? json_decode($body, true) : null;
    if (!is_array($response)) throw new RuntimeException('Meal export HTTP response not JSON');
    return $response;
};
$shape = static function ($value) use (&$shape): string {
    if (!is_array($value)) return gettype($value);
    $keys = array_keys($value);
    if ($keys === range(0, count($keys) - 1)) {
        return '[' . implode(',', array_map($shape, $value)) . ']';
    }
    sort($keys, SORT_STRING);
    $parts = [];
    foreach ($keys as $key) $parts[] = $key . ':' . $shape($value[$key]);
    return '{' . implode(',', $parts) . '}';
};
$byId = $db->prepare('SELECT ID,TYPE,STATUS,FILE_NAME,FILE_PATH,CREATE_BY_UUID,QUERY_CONDITIONS'
    . ' FROM ' . $table('export_log') . ' WHERE ID=?');
$forAdmin = $db->prepare('SELECT ID FROM ' . $table('export_log')
    . ' WHERE CREATE_BY_UUID=? AND TYPE=1 ORDER BY ID DESC');
$deleteJob = $db->prepare('DELETE FROM ' . $table('export_log')
    . ' WHERE ID=? AND CREATE_BY_UUID=? AND TYPE=1');
$deleteAdmin = $db->prepare('DELETE FROM ' . $table('user')
    . ' WHERE ID=? AND UUID=? AND ACCOUNT=?');
try {
    $insert = $db->prepare('INSERT INTO ' . $table('user')
        . ' (UUID,ACCOUNT,PWD,SALT,NAME,TYPE,CREATE_TIME,CREATE_BY,IS_DELETE)'
        . ' VALUES (?,?,?,?,?,?,?,?,?)');
    $insert->execute([$admin, $admin, 'DISABLED', strtoupper(bin2hex(random_bytes(8))),
        $admin, 0, date('Y-m-d H:i:s'), 'CODEX', 0]);
    $adminRow = $read('SELECT ID FROM ' . $table('user')
        . ' WHERE UUID=? AND ACCOUNT=?', [$admin, $admin]);
    $adminId = (int)($adminRow['id'] ?? 0);
    if ($adminId <= 0) throw new RuntimeException('Synthetic admin insert failed');

    $form = ['type' => '1', 'order_no' => (string)$source['order_no'],
        'start_date' => $bindDate, 'end_date' => $bindDate,
        'nickname' => $account];
    $anonymous = $post('/p/mealOrder/export', $form, false);
    $forAdmin->execute([$admin]);
    $results['anonymous_rejected'] = [
        'status' => (int)($anonymous['code'] ?? -1) === 100
            && $forAdmin->fetchAll(PDO::FETCH_ASSOC) === [] ? 'tested-pass' : 'tested-fail',
        'message' => $anonymous['msg'] ?? $anonymous['message'] ?? null,
        'shape_sha256' => hash('sha256', $shape($anonymous))];

    $submitted = $post('/p/mealOrder/export', $form);
    $forAdmin->execute([$admin]);
    $jobs = $forAdmin->fetchAll(PDO::FETCH_ASSOC);
    if ((int)($submitted['code'] ?? -1) !== 0 || count($jobs) !== 1) {
        throw new RuntimeException('Formal meal export did not enqueue one job');
    }
    $jobId = (int)array_change_key_case($jobs[0], CASE_LOWER)['id'];
    $byId->execute([$jobId]);
    $job = array_change_key_case($byId->fetch(PDO::FETCH_ASSOC) ?: [], CASE_LOWER);
    $conditions = json_decode($dbString($job['query_conditions'] ?? ''), true);
    if ((int)($job['type'] ?? 0) !== 1 || (int)($job['status'] ?? -1) !== 0
        || (string)($job['create_by_uuid'] ?? '') !== $admin
        || ($conditions['order_no'] ?? null) !== (string)$source['order_no']
        || ($conditions['nickname'] ?? null) !== $account || $pending() !== 1) {
        throw new RuntimeException('Meal export is not the sole scoped pending job');
    }
    $results['http_enqueue'] = ['status' => 'tested-pass', 'job_type' => 1,
        'job_count' => 1, 'message' => $submitted['message'] ?? null,
        'shape_sha256' => hash('sha256', $shape($submitted))];

    $command = escapeshellarg(PHP_BINARY) . ' -c ' . escapeshellarg((string)php_ini_loaded_file());
    if ($dialect === 'dm') {
        $dir = '/home/dmdba/dmdbms/drivers/php_pdo/';
        $command .= ' -d extension=' . escapeshellarg($dir . 'libphp73_dm.so')
            . ' -d extension=' . escapeshellarg($dir . 'php73_pdo_dm.so');
    }
    $command .= ' -d auto_prepend_file=' . escapeshellarg($stub)
        . ' ' . escapeshellarg($root . '/think') . ' ExportMealOrder';
    $proc = proc_open($command,
        [0 => ['file', '/dev/null', 'r'], 1 => ['pipe', 'w'], 2 => ['pipe', 'w']],
        $pipes, $root);
    if (!is_resource($proc)) throw new RuntimeException('Meal export CLI did not start');
    $stdout = stream_get_contents($pipes[1]);
    $stderr = stream_get_contents($pipes[2]);
    fclose($pipes[1]);
    fclose($pipes[2]);
    $exit = proc_close($proc);
    $byId->execute([$jobId]);
    $job = array_change_key_case($byId->fetch(PDO::FETCH_ASSOC) ?: [], CASE_LOWER);
    if ($exit !== 0 || (int)($job['status'] ?? -1) !== 1
        || $dbString($job['file_name'] ?? '') === '' || $dbString($job['file_path'] ?? '') === '') {
        throw new RuntimeException('ExportMealOrder type1 failed; status=' . ($job['status'] ?? 'missing')
            . ', exit=' . $exit . ', stdout_bytes=' . strlen((string)$stdout)
            . ', stderr_bytes=' . strlen((string)$stderr));
    }
    $results['worker_complete'] = ['status' => 'tested-pass',
        'job_status' => 1, 'worker_exit' => 0, 'pending_jobs' => $pending()];

    $name = $dbString($job['file_name']);
    if (preg_match('#^[^/]+_' . $jobId . '\.xlsx$#uD', $name)) {
        $file = $dateDir . '/' . $name; // Exact job-ID fallback for finally cleanup.
    }
    $path = parse_url($dbString($job['file_path']), PHP_URL_PATH);
    if (!is_string($path)
        || preg_match('#^/static/download/[0-9]{8}/[^/]+_' . $jobId . '\.xlsx$#uD', $path) !== 1
        || substr($path, strrpos($path, '/') + 1) !== $name || $file === '') {
        throw new RuntimeException('Meal export path is not this private job');
    }
    $file = $root . '/public' . $path;
    if (realpath($file) !== $file || strpos($file, $dateDir . '/') !== 0) {
        throw new RuntimeException('Meal XLSX outside private runtime');
    }
    $zip = new ZipArchive();
    if ($zip->open($file) !== true) throw new RuntimeException('Meal XLSX ZIP invalid');
    $sheet = $zip->getFromName('xl/worksheets/sheet1.xml');
    $zip->close();
    $xml = is_string($sheet) ? simplexml_load_string($sheet) : false;
    if (!$xml) throw new RuntimeException('Meal XLSX worksheet XML invalid');
    $ns = 'http://schemas.openxmlformats.org/spreadsheetml/2006/main';
    $xml->registerXPathNamespace('x', $ns);
    $rows = $xml->xpath('//x:sheetData/x:row');
    if (!is_array($rows) || count($rows) !== 3) {
        throw new RuntimeException('Meal XLSX must contain header, one order and total row');
    }
    $cellMap = static function ($xmlRow) use ($ns): array {
        $xmlRow->registerXPathNamespace('x', $ns);
        $result = [];
        foreach ($xmlRow->xpath('./x:c') as $cell) {
            $cell->registerXPathNamespace('x', $ns);
            $column = preg_replace('/[0-9]+/', '', (string)$cell['r']);
            $inline = $cell->xpath('./x:is/x:t');
            $numeric = $cell->xpath('./x:v');
            $result[$column] = isset($inline[0]) ? (string)$inline[0]
                : (isset($numeric[0]) ? (string)$numeric[0] : '');
        }
        return $result;
    };
    $header = $cellMap($rows[0]);
    if (count($header) !== 42 || ($header['A'] ?? null) !== '序号'
        || ($header['B'] ?? null) !== '订单编号'
        || ($header['Z'] ?? null) !== '退款金额'
        || ($header['AP'] ?? null) !== '取餐柜取餐时间') {
        throw new RuntimeException('Meal XLSX default 42-column header changed');
    }
    $row = $cellMap($rows[1]);
    $total = $cellMap($rows[2]);
    $payNet = bcsub((string)$source['pay_price'], (string)$source['refund_amount'], 2);
    $giftNet = bcsub((string)$source['gift'], (string)$source['gift_refund_amount'], 2);
    $money = ['Q' => 'order_price', 'R' => 'discount_amount', 'S' => 'management_fee',
        'U' => 'cash', 'V' => 'subsidy', 'W' => 'gift',
        'X' => 'gift_refund_amount', 'Z' => 'refund_amount'];
    $matching = ($row['B'] ?? null) === (string)$source['order_no']
        && ($row['C'] ?? null) === $account
        && (int)($row['P'] ?? -1) === (int)$source['meal_number']
        && bccomp((string)($row['T'] ?? '0'), $payNet, 2) === 0
        && bccomp((string)($row['Y'] ?? '0'), $giftNet, 2) === 0
        && ($total['A'] ?? null) === '合计'
        && (int)($total['P'] ?? -1) === (int)$source['meal_number'];
    foreach ($money as $column => $field) {
        $matching = $matching
            && bccomp((string)($row[$column] ?? '0'), (string)$source[$field], 2) === 0
            && bccomp((string)($total[$column] ?? '0'), (string)$source[$field], 2) === 0;
    }
    $matching = $matching
        && bccomp((string)($total['T'] ?? '0'), $payNet, 2) === 0
        && bccomp((string)($total['Y'] ?? '0'), $giftNet, 2) === 0;
    if (!$matching) throw new RuntimeException('Meal XLSX amounts/totals differ from source order');
    $results['xlsx_readback'] = ['status' => 'tested-pass', 'business_rows' => 1,
        'total_rows' => 1, 'file_bytes' => filesize($file),
        'header_columns' => count($header),
        'header_sha256' => hash('sha256', json_encode($header, JSON_UNESCAPED_UNICODE)),
        'order_money_and_totals_match' => true];

    $logs = $post('/p/mealOrder/exportLogs', ['type' => 1, 'page' => 1, 'page_size' => 5]);
    $items = (array)($logs['data']['data'] ?? []);
    $matches = array_values(array_filter($items, static function ($item) use ($jobId): bool {
        return is_array($item) && (int)($item['id'] ?? 0) === $jobId;
    }));
    $results['http_logs'] = ['status' => (int)($logs['code'] ?? -1) === 0
        && count($matches) === 1 && (int)($matches[0]['status'] ?? -1) === 1
        && (string)($matches[0]['url'] ?? '') === $dbString($job['file_path'])
        ? 'tested-pass' : 'tested-fail',
        'matching_job_rows' => count($matches),
        'shape_sha256' => hash('sha256', $shape($logs))];
} finally {
    if ($file !== '' && is_file($file) && strpos($file, $dateDir . '/') === 0
        && !is_link($file)) unlink($file);
    if (is_dir($dateDir)) rmdir($dateDir);
    if (is_dir($downloadRoot) && !$rootExisted) rmdir($downloadRoot);
    if ($jobId > 0) $deleteJob->execute([$jobId, $admin]);
    if ($adminId > 0) $deleteAdmin->execute([$adminId, $admin, $admin]);
    if ($jobId > 0 && $read('SELECT ID FROM ' . $table('export_log')
        . ' WHERE ID=?', [$jobId])) throw new RuntimeException('Meal export job not cleaned');
    if (is_dir($dateDir)) throw new RuntimeException('Meal export directory not cleaned');
}
$results['cleanup'] = ['status' => 'tested-pass', 'job_rows' => 0,
    'operator_rows' => 0, 'file_exists' => false];
echo json_encode(['dialect' => $dialect, 'module' => 'store-meal-export-type1',
    'results' => $results], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL;
