<?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_RECHARGE_ACCOUNT');
$ossStub = (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($ossStub) || !is_file($root . '/application/base/Security.php')) {
    throw new RuntimeException('Isolated recharge export 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('Isolated HTTP/Redis boundary absent');
}
$downloadRoot = $root . '/public/static/download';
$dateDir = $downloadRoot . '/' . date('Ymd');
$downloadRootExisted = is_dir($downloadRoot);
if (is_link($downloadRoot) || is_link($dateDir) || is_dir($dateDir)
    || (file_exists($downloadRoot) && realpath($downloadRoot) !== $downloadRoot)) {
    throw new RuntimeException('Download directory is not exclusive to this run');
}

$readRuntimeEnv = static function (string $prefix): array {
    $keys = ['HOST', 'PORT', 'SCHEMA', 'USER', 'PASSWORD'];
    $values = [];
    foreach ($keys as $key) $values[$key] = (string)getenv($prefix . $key);
    $pid = (string)getenv('POC_RUNTIME_PID');
    if ($values['HOST'] === '' && ctype_digit($pid)) {
        foreach (explode("\0", (string)file_get_contents('/proc/' . $pid . '/environ')) as $entry) {
            $parts = explode('=', $entry, 2);
            if (count($parts) === 2 && strpos($parts[0], $prefix) === 0) {
                $key = substr($parts[0], strlen($prefix));
                if (array_key_exists($key, $values)) $values[$key] = $parts[1];
            }
        }
    }
    return $values;
};
$storeEnv = $readRuntimeEnv('POC_');
$aiEnv = $readRuntimeEnv('POC_AI_');
if ($dialect === 'mysql' && ($storeEnv['HOST'] === '' || $aiEnv['HOST'] === '')) {
    $local = $root . '/application/config/local/database.php';
    $config = require (is_file($local) ? $local : $root . '/application/database.php');
    $aiConfig = $config['ai_zhct'];
    $storeEnv = ['HOST' => (string)$config['hostname'],
        'PORT' => (string)($config['hostport'] ?? 3306),
        'SCHEMA' => (string)$config['database'],
        'USER' => (string)$config['username'],
        'PASSWORD' => (string)$config['password']];
    $aiEnv = ['HOST' => (string)($aiConfig['hostname'] ?? $config['hostname']),
        'PORT' => (string)($aiConfig['hostport'] ?? $config['hostport'] ?? 3306),
        'SCHEMA' => (string)$aiConfig['database'],
        'USER' => (string)($aiConfig['username'] ?? $config['username']),
        'PASSWORD' => (string)($aiConfig['password'] ?? $config['password'])];
}
$connect = static function (array $config) use ($dialect): PDO {
    if ($config['HOST'] === '' || $config['SCHEMA'] === '' || $config['USER'] === '') {
        throw new RuntimeException('POC DB settings unavailable');
    }
    $dsn = $dialect === 'dm'
        ? "dm:host={$config['HOST']};port={$config['PORT']};schema={$config['SCHEMA']}"
        : "mysql:host={$config['HOST']};port={$config['PORT']};dbname={$config['SCHEMA']};charset=utf8mb4";
    return new PDO($dsn, $config['USER'], $config['PASSWORD'],
        [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
};
$store = $connect($storeEnv);
$ai = $connect($aiEnv);
$s = static function (string $name) use ($dialect): string {
    return ($dialect === 'dm' ? 'YDY_' . strtoupper($name) : 'ydy_' . $name);
};
$a = static function (string $name) use ($dialect): string {
    return ($dialect === 'dm' ? 'YOSHOP_' . strtoupper($name) : 'yoshop_' . $name);
};
$dbString = static function ($value): string {
    return is_resource($value) ? (string)stream_get_contents($value) : (string)$value;
};
$read = static function (PDO $pdo, string $sql, array $params = []) use ($dbString): array {
    $stmt = $pdo->prepare($sql);
    $stmt->execute($params);
    $row = array_change_key_case($stmt->fetch(PDO::FETCH_ASSOC) ?: [], CASE_LOWER);
    foreach ($row as $key => $value) {
        if (is_resource($value)) $row[$key] = $dbString($value);
    }
    return $row;
};
$pending = static function () use ($store, $s): int {
    return (int)$store->query('SELECT COUNT(*) FROM ' . $s('export_log') . ' WHERE STATUS=0')->fetchColumn();
};
if ($pending() !== 0) throw new RuntimeException('Another export job is pending');
$staff = $read($store, 'SELECT s.AI_USER_ID,s.NAME FROM ' . $s('user') . ' u'
    . ' JOIN ' . $s('staff') . ' s ON s.UUID=u.STAFF_UUID WHERE u.ACCOUNT=?', [$account]);
if ((int)($staff['ai_user_id'] ?? 0) <= 0 || (string)($staff['name'] ?? '') !== $account) {
    throw new RuntimeException('CDXPAY Store staff is not a unique synthetic source');
}
$sourceStmt = $ai->prepare('SELECT ORDER_ID,ORDER_NO,USER_ID,PAY_METHOD,PAY_PRICE,GIFT_MONEY,'
    . ' ACTUAL_MONEY,PAY_TIME,RECHARGE_DATE,PAY_STATUS FROM ' . $a('recharge_order')
    . ' WHERE USER_ID=? AND IS_DELETE=0 AND PAY_STATUS=20 ORDER BY ORDER_ID DESC '
    . ($dialect === 'dm' ? 'FETCH FIRST 2 ROWS ONLY' : 'LIMIT 2'));
$sourceStmt->execute([(int)$staff['ai_user_id']]);
$sourceRows = array_map('array_change_key_case', $sourceStmt->fetchAll(PDO::FETCH_ASSOC));
if (count($sourceRows) !== 1) throw new RuntimeException('One completed CDXPAY recharge is required');
$source = $sourceRows[0];
$date = substr((string)$source['recharge_date'], 0, 10);

require_once $root . '/application/base/Security.php';
$admin = 'CDXPARSTREXP' . strtoupper(bin2hex(random_bytes(6)));
$adminId = 0;
$jobId = 0;
$filePath = '';
$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('Recharge export HTTP status mismatch');
    }
    $json = is_string($body) ? json_decode($body, true) : null;
    if (!is_array($json)) throw new RuntimeException('Recharge export HTTP did not return JSON');
    return $json;
};
$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) . '}';
};
$jobById = $store->prepare('SELECT ID,TYPE,STATUS,FILE_NAME,FILE_PATH,CREATE_BY_UUID,QUERY_CONDITIONS'
    . ' FROM ' . $s('export_log') . ' WHERE ID=?');
$jobsForAdmin = $store->prepare('SELECT ID FROM ' . $s('export_log')
    . ' WHERE CREATE_BY_UUID=? AND TYPE=12 ORDER BY ID DESC');
$deleteJob = $store->prepare('DELETE FROM ' . $s('export_log') . ' WHERE ID=? AND CREATE_BY_UUID=? AND TYPE=12');
$deleteAdmin = $store->prepare('DELETE FROM ' . $s('user') . ' WHERE ID=? AND UUID=? AND ACCOUNT=?');
try {
    $insert = $store->prepare('INSERT INTO ' . $s('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($store, 'SELECT ID FROM ' . $s('user') . ' WHERE UUID=? AND ACCOUNT=?', [$admin, $admin]);
    $adminId = (int)($adminRow['id'] ?? 0);
    if ($adminId <= 0) throw new RuntimeException('Synthetic recharge exporter not inserted');

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

    $submitted = $post('/p/rechargeOrder/export', $form);
    $jobsForAdmin->execute([$admin]);
    $jobs = $jobsForAdmin->fetchAll(PDO::FETCH_ASSOC);
    if ((int)($submitted['code'] ?? -1) !== 0 || count($jobs) !== 1) {
        throw new RuntimeException('Recharge export HTTP did not enqueue one job');
    }
    $jobId = (int)array_change_key_case($jobs[0], CASE_LOWER)['id'];
    $jobById->execute([$jobId]);
    $job = array_change_key_case($jobById->fetch(PDO::FETCH_ASSOC) ?: [], CASE_LOWER);
    $conditions = json_decode($dbString($job['query_conditions'] ?? ''), true);
    if ((int)($job['type'] ?? 0) !== 12 || (int)($job['status'] ?? -1) !== 0
        || (string)($job['create_by_uuid'] ?? '') !== $admin
        || ($conditions['nickname'] ?? null) !== $account
        || ($conditions['pay_method'] ?? null) !== (string)$source['pay_method']
        || $pending() !== 1) {
        throw new RuntimeException('Recharge export job is not the sole pending work');
    }
    $results['http_enqueue'] = ['status' => 'tested-pass', 'job_type' => 12,
        '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($ossStub)
        . ' ' . 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('Export CLI did not launch');
    $stdout = stream_get_contents($pipes[1]);
    $stderr = stream_get_contents($pipes[2]);
    fclose($pipes[1]);
    fclose($pipes[2]);
    $exit = proc_close($proc);
    $jobById->execute([$jobId]);
    $job = array_change_key_case($jobById->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('Recharge ExportMealOrder 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)) {
        $filePath = $dateDir . '/' . $name; // Exact job-ID cleanup fallback.
    }
    $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
        || $filePath === '') {
        throw new RuntimeException('Recharge XLSX path is not this isolated job');
    }
    $filePath = $root . '/public' . $path;
    if (realpath($filePath) !== $filePath || strpos($filePath, $dateDir . '/') !== 0) {
        throw new RuntimeException('Recharge XLSX not in private runtime directory');
    }
    $zip = new ZipArchive();
    if ($zip->open($filePath) !== true) throw new RuntimeException('Recharge 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('Recharge 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) !== 2) {
        throw new RuntimeException('Recharge XLSX must contain header and one business row');
    }
    $rows[1]->registerXPathNamespace('x', $ns);
    $cells = [];
    foreach ($rows[1]->xpath('./x:c') as $cell) {
        $cell->registerXPathNamespace('x', $ns);
        $col = preg_replace('/[0-9]+/', '', (string)$cell['r']);
        $inline = $cell->xpath('./x:is/x:t');
        $numeric = $cell->xpath('./x:v');
        $cells[$col] = isset($inline[0]) ? (string)$inline[0]
            : (isset($numeric[0]) ? (string)$numeric[0] : '');
    }
    if (($cells['B'] ?? null) !== (string)$source['order_no']
        || ($cells['C'] ?? null) !== $account
        || bccomp((string)($cells['E'] ?? '0'), (string)$source['pay_price'], 2) !== 0
        || bccomp((string)($cells['F'] ?? '0'), (string)$source['gift_money'], 2) !== 0
        || bccomp((string)($cells['G'] ?? '0'), (string)$source['actual_money'], 2) !== 0
        || ($cells['I'] ?? null) !== date('Y-m-d H:i:s', (int)$source['pay_time'])) {
        throw new RuntimeException('Recharge XLSX source fields/amounts differ');
    }
    $results['xlsx_readback'] = ['status' => 'tested-pass', 'business_rows' => 1,
        'file_bytes' => filesize($filePath), 'three_amounts_match_source' => true];

    $logs = $post('/p/rechargeOrder/exportLogs', ['page' => 1, 'page_size' => 5]);
    $list = (array)($logs['data']['data'] ?? []);
    $hits = array_values(array_filter($list, 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($hits) === 1 && (int)($hits[0]['status'] ?? -1) === 1
        && (string)($hits[0]['url'] ?? '') === $dbString($job['file_path'])
        ? 'tested-pass' : 'tested-fail',
        'matching_job_rows' => count($hits),
        'shape_sha256' => hash('sha256', $shape($logs))];
} finally {
    if ($filePath !== '' && is_file($filePath)
        && strpos($filePath, $dateDir . '/') === 0 && !is_link($filePath)) unlink($filePath);
    if (is_dir($dateDir)) rmdir($dateDir);
    if (is_dir($downloadRoot) && !$downloadRootExisted) rmdir($downloadRoot);
    if ($jobId > 0) $deleteJob->execute([$jobId, $admin]);
    if ($adminId > 0) $deleteAdmin->execute([$adminId, $admin, $admin]);
    if ($jobId > 0 && $read($store, 'SELECT ID FROM ' . $s('export_log') . ' WHERE ID=?', [$jobId])) {
        throw new RuntimeException('Recharge export test job not cleaned');
    }
    if (is_dir($dateDir)) throw new RuntimeException('Recharge export test file directory not cleaned');
}
$results['cleanup'] = ['status' => 'tested-pass', 'job_rows' => 0,
    'operator_rows' => 0, 'file_exists' => false];
echo json_encode(['dialect' => $dialect, 'module' => 'store-recharge-export',
    'results' => $results], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL;
