<?php
/**
 * API 统一入口：由各桩文件定义 API_MODULE 后引入，集中处理所有接口逻辑
 */
if (!defined('API_MODULE')) {
    header('Content-Type: application/json; charset=utf-8');
    echo json_encode(['code' => 400, 'msg' => 'Invalid API module']);
    exit;
}

ob_start();
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');

if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
    exit(0);
}

require_once dirname(__DIR__) . '/auth/bootstrap.php';

$input = $_SERVER['REQUEST_METHOD'] === 'POST'
    ? (json_decode(file_get_contents('php://input'), true) ?: $_POST)
    : $_GET;
if ($_SERVER['REQUEST_METHOD'] === 'POST' && empty($input)) {
    $input = json_decode(file_get_contents('php://input'), true) ?: [];
}
$action = isset($_GET['action']) ? trim($_GET['action']) : '';
if ($action === '' && isset($input['action'])) $action = trim((string) $input['action']);

function apiJsonResp($code, $msg, $data = null) {
    if (ob_get_length()) ob_end_clean();
    $out = ['code' => (int) $code, 'msg' => $msg];
    if ($data !== null) $out['data'] = $data;
    echo json_encode($out, JSON_UNESCAPED_UNICODE);
}

/**
 * 通过8899端口API封禁IP
 * @param string $ip IP地址
 * @param string $reason 封禁原因
 * @return array ['success' => bool, 'error' => string]
 */
function blockIPViaPortLogger($ip, $reason = '黑名单封禁') {
    $hostname = $_SERVER['HTTP_HOST'] ?? 'localhost';
    $port = 8899;
    
    // 解析主机名（去除端口号）
    if (strpos($hostname, ':') !== false) {
        $hostname = explode(':', $hostname)[0];
    }
    
    $url = "http://{$hostname}:{$port}/api/block";
    
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $url);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode(['ip' => $ip, 'reason' => $reason]));
    curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
    curl_setopt($ch, CURLOPT_TIMEOUT, 60);
    curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);
    
    $response = curl_exec($ch);
    $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    $error = curl_error($ch);
    curl_close($ch);
    
    if ($error) {
        return ['success' => false, 'error' => '连接失败: ' . $error];
    }
    
    if ($http_code !== 200) {
        return ['success' => false, 'error' => 'HTTP ' . $http_code];
    }
    
    $data = json_decode($response, true);
    if ($data && isset($data['success']) && $data['success']) {
        return ['success' => true, 'error' => ''];
    } else {
        $error_msg = isset($data['error']) ? $data['error'] : '未知错误';
        return ['success' => false, 'error' => $error_msg];
    }
}

/**
 * 通过8899端口API解封IP
 * @param string $ip IP地址
 * @return array ['success' => bool, 'error' => string]
 */
function unblockIPViaPortLogger($ip) {
    $hostname = $_SERVER['HTTP_HOST'] ?? 'localhost';
    $port = 8899;
    
    // 解析主机名（去除端口号）
    if (strpos($hostname, ':') !== false) {
        $hostname = explode(':', $hostname)[0];
    }
    
    $url = "http://{$hostname}:{$port}/api/unblock";
    
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $url);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode(['ip' => $ip]));
    curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
    curl_setopt($ch, CURLOPT_TIMEOUT, 60);
    curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);
    
    $response = curl_exec($ch);
    $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    $error = curl_error($ch);
    curl_close($ch);
    
    if ($error) {
        return ['success' => false, 'error' => '连接失败: ' . $error];
    }
    
    if ($http_code !== 200) {
        return ['success' => false, 'error' => 'HTTP ' . $http_code];
    }
    
    $data = json_decode($response, true);
    if ($data && isset($data['success']) && $data['success']) {
        return ['success' => true, 'error' => ''];
    } else {
        $error_msg = isset($data['error']) ? $data['error'] : '未知错误';
        return ['success' => false, 'error' => $error_msg];
    }
}

/**
 * 从 port_logger 获取最近一段时间内在指定端口有连接的 IP 列表
 * @param int $port 监听的目标端口（例如 9999）
 * @param int $seconds 窗口时间（秒），只返回这段时间内有连接的 IP
 * @return array ['1.2.3.4' => true, ...]
 */
function getRecentPortLoggerIps($port = 9999, $seconds = 600) {
    $hostname = $_SERVER['HTTP_HOST'] ?? 'localhost';
    // 解析主机名（去除端口号）
    if (strpos($hostname, ':') !== false) {
        $hostname = explode(':', $hostname)[0];
    }
    // 端口日志 Web 服务端口，需与 port_logger.sh 中 WEB_PORT 保持一致
    $webPort = 8899;
    $url = "http://{$hostname}:{$webPort}/api/logs";

    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $url);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_TIMEOUT, 5);
    curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 3);

    $response = curl_exec($ch);
    $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    if ($response === false || $http_code !== 200) {
        return [];
    }

    $data = json_decode($response, true);
    if (!is_array($data) || !isset($data['logs']) || !is_array($data['logs'])) {
        return [];
    }

    $now = time();
    $active = [];
    foreach ($data['logs'] as $entry) {
        if (!isset($entry['ip'], $entry['port'], $entry['time'])) continue;
        if ((int)$entry['port'] !== (int)$port) continue;
        $ip = trim((string)$entry['ip']);
        if ($ip === '') continue;
        $ts = strtotime($entry['time']);
        if ($ts === false) continue;
        if (($now - $ts) <= $seconds) {
            $active[$ip] = true;
        }
    }
    return $active;
}

switch (API_MODULE) {

// ---------- 管理员登录 ----------
case 'admin_login':
    if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
        apiJsonResp(405, '请使用 POST');
        exit;
    }
    $username = isset($input['username']) ? trim($input['username']) : '';
    $password = isset($input['password']) ? $input['password'] : '';
    if ($username === '' || $password === '') {
        apiJsonResp(400, '用户名和密码不能为空');
        exit;
    }
    $stmt = $pdo->prepare('SELECT id, username, password, role FROM admin_users WHERE username = ? LIMIT 1');
    $stmt->execute([$username]);
    $user = $stmt->fetch();
    if (!$user || !password_verify($password, $user['password'])) {
        apiJsonResp(403, '用户名或密码错误');
        exit;
    }
    $_SESSION['admin_user_id'] = (int) $user['id'];
    $_SESSION['admin_username'] = $user['username'];
    $_SESSION['admin_role'] = isset($user['role']) && $user['role'] === 'agent' ? 'agent' : 'admin';
    try {
        $pdo->prepare('UPDATE admin_users SET current_session_id = ? WHERE id = ?')->execute([session_id(), (int) $user['id']]);
    } catch (Exception $e) {}
    try {
        $ip = getClientIp();
        adminLog($pdo, '管理员登录', 'ip=' . $ip);
    } catch (Exception $e) {}
    apiJsonResp(0, '登录成功', ['username' => $user['username']]);
    break;

// ---------- 管理员登出（API，若前端调用） ----------
case 'admin_logout':
    $_SESSION = [];
    if (ini_get('session.use_cookies')) {
        $p = session_get_cookie_params();
        setcookie(session_name(), '', time() - 42000, $p['path'], $p['domain'], $p['secure'], $p['httponly']);
    }
    session_destroy();
    apiJsonResp(0, '已退出');
    break;

// ---------- 用户登录 ----------
case 'user_login':
    if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
        apiJsonResp(405, '请使用 POST');
        exit;
    }
    $username = isset($input['username']) ? trim($input['username']) : '';
    $password = isset($input['password']) ? $input['password'] : '';
    if ($username === '' || $password === '') {
        apiJsonResp(400, '用户名和密码不能为空');
        exit;
    }
    $client_ip = getClientIp();
    $bl = checkBlacklist($pdo, $client_ip, $username);
    if ($bl['blocked']) {
        apiJsonResp(403, $bl['reason']);
        exit;
    }
    $stmt = $pdo->prepare('SELECT id, username, password, card_key_id FROM users WHERE username = ? LIMIT 1');
    $stmt->execute([$username]);
    $user = $stmt->fetch();
    if (!$user || !password_verify($password, $user['password'])) {
        apiJsonResp(403, '用户名或密码错误');
        exit;
    }
    $user_id = (int) $user['id'];
    try {
        $stmt = $pdo->prepare('INSERT INTO user_login_log (user_id, ip) VALUES (?, ?)');
        $stmt->execute([$user_id, $client_ip]);
    } catch (PDOException $e) {}
    $_SESSION['user_id'] = $user_id;
    $_SESSION['username'] = $user['username'];
    apiJsonResp(0, '登录成功', ['username' => $user['username']]);
    break;

// ---------- 用户登出 ----------
case 'user_logout':
    unset($_SESSION['user_id'], $_SESSION['username']);
    apiJsonResp(0, '已退出');
    break;

// ---------- 访问校验 ----------
case 'check_access':
    header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
    header('Pragma: no-cache');
    header('Expires: 0');
    if (!isUserLoggedIn()) {
        if (ob_get_length()) ob_end_clean();
        echo json_encode(['code' => 403, 'allowed' => false, 'msg' => '请先登录或注册']);
        exit;
    }
    $current_ip = getClientIp();
    $current_username = $_SESSION['username'] ?? '';
    $bl = checkBlacklist($pdo, $current_ip, $current_username);
    if ($bl['blocked']) {
        unset($_SESSION['user_id'], $_SESSION['username']);
        if (ob_get_length()) ob_end_clean();
        echo json_encode(['code' => 403, 'allowed' => false, 'msg' => $bl['reason']]);
        exit;
    }
    $user_id = (int) $_SESSION['user_id'];
    $stmt = $pdo->prepare('SELECT card_key_id FROM users WHERE id = ? LIMIT 1');
    $stmt->execute([$user_id]);
    $user = $stmt->fetch();
    $allowed = true;
    if (!$user) {
        $allowed = false;
        unset($_SESSION['user_id'], $_SESSION['username']);
    } elseif (!empty($user['card_key_id'])) {
        $stmt = $pdo->prepare('SELECT id, paused, expires_at FROM card_keys WHERE id = ? LIMIT 1');
        $stmt->execute([$user['card_key_id']]);
        $card = $stmt->fetch();
        if (!$card || !empty($card['paused']) || ($card['expires_at'] !== null && strtotime($card['expires_at']) < time())) {
            $allowed = false;
            unset($_SESSION['user_id'], $_SESSION['username']);
        }
    }
    if (ob_get_length()) ob_end_clean();
    if ($allowed) {
        echo json_encode(['code' => 0, 'allowed' => true, 'msg' => '已登录']);
    } else {
        echo json_encode(['code' => 403, 'allowed' => false, 'msg' => '卡密已失效或已过期，请重新登录或注册']);
    }
    break;

// ---------- 仅检查用户是否已登录 ----------
case 'session_check':
    header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
    if (ob_get_length()) ob_end_clean();
    if (isUserLoggedIn()) {
        echo json_encode(['code' => 0, 'logged_in' => true, 'username' => $_SESSION['username'] ?? '']);
    } else {
        echo json_encode(['code' => 0, 'logged_in' => false]);
    }
    break;

// ---------- 当前登录用户信息（含到期时间） ----------
case 'user_profile':
    header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
    if (!isUserLoggedIn()) {
        apiJsonResp(401, '未登录');
        exit;
    }
    $user_id = (int) ($_SESSION['user_id'] ?? 0);
    if ($user_id <= 0) {
        apiJsonResp(401, '未登录');
        exit;
    }
    $stmt = $pdo->prepare('SELECT u.username, u.card_key_id, c.card_code, c.expires_at, c.paused AS card_paused FROM users u LEFT JOIN card_keys c ON c.id = u.card_key_id WHERE u.id = ? LIMIT 1');
    $stmt->execute([$user_id]);
    $row = $stmt->fetch();
    if (!$row) {
        apiJsonResp(404, '用户不存在');
        exit;
    }
    $expires_at = $row['expires_at'] ?: null;
    $card_status = '未绑定';
    if (!empty($row['card_key_id'])) {
        if (empty($row['card_code'])) {
            $card_status = '已删除/已过期';
        } elseif ($expires_at && strtotime($expires_at) < time()) {
            $card_status = '已过期';
        } elseif (!empty($row['card_paused'])) {
            $card_status = '已暂停';
        } else {
            $card_status = '正常';
        }
    }
    apiJsonResp(0, 'ok', [
        'username' => $row['username'],
        'card_code' => $row['card_code'] ?: null,
        'expires_at' => $expires_at,
        'card_status' => $card_status,
    ]);
    break;

// ---------- 注册 ----------
case 'register':
    if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
        apiJsonResp(405, '请使用 POST');
        exit;
    }
    $username = isset($input['username']) ? trim($input['username']) : '';
    $password = isset($input['password']) ? $input['password'] : '';
    $card_code = isset($input['card_code']) ? trim($input['card_code']) : '';
    $security_code = isset($input['security_code']) ? (string) $input['security_code'] : '';
    $client_ip = getClientIp();
    $bl = checkBlacklist($pdo, $client_ip, $username);
    if ($bl['blocked']) {
        apiJsonResp(403, $bl['reason']);
        exit;
    }
    if (mb_strlen($username) < 4) {
        apiJsonResp(400, '用户名至少4位');
        exit;
    }
    if ($password === '' || strlen($password) < 4) {
        apiJsonResp(400, '密码至少4位');
        exit;
    }
    if ($card_code === '') {
        apiJsonResp(400, '请输入卡密');
        exit;
    }
    if ($security_code === '' || strlen($security_code) < 4) {
        apiJsonResp(400, '安全码至少4位');
        exit;
    }
    $stmt = $pdo->prepare('SELECT id FROM card_keys WHERE card_code = ? LIMIT 1');
    $stmt->execute([$card_code]);
    $card = $stmt->fetch();
    if (!$card) {
        apiJsonResp(404, '卡密不存在');
        exit;
    }
    $stmt = $pdo->prepare('SELECT id, status, paused, expires_at, card_type FROM card_keys WHERE id = ? LIMIT 1');
    $stmt->execute([$card['id']]);
    $card = $stmt->fetch();
    if ((int) $card['status'] === 1) {
        apiJsonResp(403, '该卡密已被使用');
        exit;
    }
    if (!empty($card['paused'])) {
        apiJsonResp(403, '该卡密已暂停');
        exit;
    }
    if ($card['expires_at'] !== null && strtotime($card['expires_at']) < time()) {
        apiJsonResp(403, '卡密已过期');
        exit;
    }
    $stmt = $pdo->prepare('SELECT id FROM users WHERE username = ? LIMIT 1');
    $stmt->execute([$username]);
    if ($stmt->fetch()) {
        apiJsonResp(409, '用户名已被占用');
        exit;
    }
    $password_hash = password_hash($password, PASSWORD_DEFAULT);
    $security_hash = password_hash($security_code, PASSWORD_DEFAULT);
    $bound_at = date('Y-m-d H:i:s');
    $expires_at = $card['expires_at'];
    if (isset($card['card_type'])) {
        if ($card['card_type'] === 'day') $expires_at = date('Y-m-d H:i:s', strtotime($bound_at) + 24 * 3600);
        elseif ($card['card_type'] === 'week') $expires_at = date('Y-m-d H:i:s', strtotime($bound_at) + 7 * 24 * 3600);
        elseif ($card['card_type'] === 'month') $expires_at = date('Y-m-d H:i:s', strtotime($bound_at) + 30 * 24 * 3600);
    }
    try {
        $pdo->beginTransaction();
        $stmt = $pdo->prepare('INSERT INTO users (username, password, security_code, card_key_id) VALUES (?, ?, ?, ?)');
        $stmt->execute([$username, $password_hash, $security_hash, $card['id']]);
        $user_id = (int) $pdo->lastInsertId();
        $stmt = $pdo->prepare('UPDATE card_keys SET status = 1, user_id = ?, bound_ip = ?, bound_at = ?, expires_at = ? WHERE id = ?');
        $stmt->execute([$user_id, $client_ip, $bound_at, $expires_at, $card['id']]);
        $stmt = $pdo->prepare('INSERT INTO user_login_log (user_id, ip) VALUES (?, ?)');
        $stmt->execute([$user_id, $client_ip]);
        $pdo->commit();
    } catch (Exception $e) {
        $pdo->rollBack();
        apiJsonResp(500, '注册失败: ' . $e->getMessage());
        exit;
    }
    $_SESSION['user_id'] = $user_id;
    $_SESSION['username'] = $username;
    apiJsonResp(0, '注册成功', ['username' => $username]);
    break;

// ---------- 用户通过安全码重置密码 ----------
case 'user_reset_password':
    if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
        apiJsonResp(405, '请使用 POST');
        exit;
    }
    $username = isset($input['username']) ? trim((string) $input['username']) : '';
    $security_code = isset($input['security_code']) ? (string) $input['security_code'] : '';
    $new_password = isset($input['new_password']) ? (string) $input['new_password'] : '';
    if ($username === '') {
        apiJsonResp(400, '请输入用户名');
        exit;
    }
    if ($security_code === '' || strlen($security_code) < 4) {
        apiJsonResp(400, '安全码至少4位');
        exit;
    }
    if ($new_password === '' || strlen($new_password) < 4) {
        apiJsonResp(400, '新密码至少4位');
        exit;
    }
    $stmt = $pdo->prepare('SELECT id, security_code FROM users WHERE username = ? LIMIT 1');
    $stmt->execute([$username]);
    $user = $stmt->fetch();
    if (!$user) {
        apiJsonResp(404, '用户不存在');
        exit;
    }
    if (empty($user['security_code']) || !password_verify($security_code, $user['security_code'])) {
        apiJsonResp(403, '安全码错误');
        exit;
    }
    $new_hash = password_hash($new_password, PASSWORD_DEFAULT);
    $stmt = $pdo->prepare('UPDATE users SET password = ? WHERE id = ?');
    $stmt->execute([$new_hash, (int) $user['id']]);
    if ($stmt->rowCount() <= 0) {
        apiJsonResp(500, '密码未修改');
        exit;
    }
    // 清理前端登录状态
    unset($_SESSION['user_id'], $_SESSION['username']);
    apiJsonResp(0, '密码已重置，请使用新密码登录');
    break;

// ---------- 根据 IP 查询最近登录用户名（用于前台房间列表显示） ----------
case 'ip_username':
    $ip = isset($_GET['ip']) ? trim((string) $_GET['ip']) : (isset($input['ip']) ? trim((string) $input['ip']) : '');
    if ($ip === '') {
        apiJsonResp(400, '缺少 ip 参数');
        exit;
    }
    try {
        $stmt = $pdo->prepare('SELECT u.username FROM user_login_log l INNER JOIN users u ON u.id = l.user_id WHERE l.ip = ? ORDER BY l.login_at DESC LIMIT 1');
        $stmt->execute([$ip]);
        $row = $stmt->fetch();
        if (!$row) {
            apiJsonResp(404, '没有找到该 IP 对应的用户');
        } else {
            apiJsonResp(0, 'ok', ['ip' => $ip, 'username' => $row['username']]);
        }
    } catch (PDOException $e) {
        apiJsonResp(500, '查询失败: ' . $e->getMessage());
    }
    break;

// ---------- WebSocket 房间上报（home_server -> 后台在线状态） ----------
case 'room_report':
    if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
        apiJsonResp(405, '请使用 POST');
        exit;
    }
    // 允许 jar 主动上报，也可以不传 ip，后端自动取请求来源 IP
    $ip = isset($input['ip']) ? trim((string) $input['ip']) : getClientIp();
    $room = isset($input['room']) ? trim((string) $input['room']) : '';
    if ($room === '') {
        apiJsonResp(400, '缺少 room 参数');
        exit;
    }
    if ($ip === '') {
        $ip = getClientIp();
    }
    try {
        // 按需创建表：记录 IP 最近所在房间
        $pdo->exec("
            CREATE TABLE IF NOT EXISTS user_room_online (
                id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
                ip VARCHAR(64) NOT NULL,
                room VARCHAR(255) NOT NULL,
                updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
                KEY idx_ip_updated_at (ip, updated_at)
            ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
        ");
        $stmt = $pdo->prepare('INSERT INTO user_room_online (ip, room, updated_at) VALUES (?, ?, NOW())');
        $stmt->execute([$ip, $room]);
        apiJsonResp(0, 'ok', ['ip' => $ip, 'room' => $room]);
    } catch (PDOException $e) {
        apiJsonResp(500, '上报失败: ' . $e->getMessage());
    }
    break;

// ---------- 卡密激活 ----------
case 'activate_card':
    if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
        apiJsonResp(405, '请使用 POST');
        exit;
    }
    $username = isset($input['username']) ? trim($input['username']) : '';
    $card_code = isset($input['card_code']) ? trim($input['card_code']) : '';
    $client_ip = getClientIp();
    $bl = checkBlacklist($pdo, $client_ip, $username);
    if ($bl['blocked']) {
        apiJsonResp(403, $bl['reason']);
        exit;
    }
    if ($username === '' || $card_code === '') {
        apiJsonResp(400, $username === '' ? '请输入用户名' : '请输入卡密');
        exit;
    }
    $stmt = $pdo->prepare('SELECT id FROM users WHERE username = ? LIMIT 1');
    $stmt->execute([$username]);
    $user = $stmt->fetch();
    if (!$user) {
        apiJsonResp(404, '用户名不存在，请先注册');
        exit;
    }
    $user_id = (int) $user['id'];
    $stmt = $pdo->prepare('SELECT id, status, paused, expires_at, card_type FROM card_keys WHERE card_code = ? LIMIT 1');
    $stmt->execute([$card_code]);
    $card = $stmt->fetch();
    if (!$card) {
        apiJsonResp(404, '卡密不存在');
        exit;
    }
    if ((int) $card['status'] === 1) {
        apiJsonResp(403, '该卡密已被使用');
        exit;
    }
    if (!empty($card['paused'])) {
        apiJsonResp(403, '该卡密已暂停');
        exit;
    }
    if ($card['expires_at'] !== null && strtotime($card['expires_at']) < time()) {
        apiJsonResp(403, '卡密已过期');
        exit;
    }
    $bound_at = date('Y-m-d H:i:s');
    $expires_at = $card['expires_at'];
    if (isset($card['card_type'])) {
        if ($card['card_type'] === 'day') $expires_at = date('Y-m-d H:i:s', strtotime($bound_at) + 24 * 3600);
        elseif ($card['card_type'] === 'week') $expires_at = date('Y-m-d H:i:s', strtotime($bound_at) + 7 * 24 * 3600);
        elseif ($card['card_type'] === 'month') $expires_at = date('Y-m-d H:i:s', strtotime($bound_at) + 30 * 24 * 3600);
    }
    try {
        $pdo->beginTransaction();
        $stmt = $pdo->prepare('UPDATE card_keys SET status = 1, user_id = ?, bound_ip = ?, bound_at = ?, expires_at = ? WHERE id = ?');
        $stmt->execute([$user_id, $client_ip, $bound_at, $expires_at, $card['id']]);
        $stmt = $pdo->prepare('UPDATE users SET card_key_id = ? WHERE id = ?');
        $stmt->execute([$card['id'], $user_id]);
        $pdo->commit();
    } catch (Exception $e) {
        $pdo->rollBack();
        apiJsonResp(500, '激活失败: ' . $e->getMessage());
        exit;
    }
    apiJsonResp(0, '激活成功，该账号有效期已更新', ['username' => $username, 'expires_at' => $expires_at]);
    break;

// ---------- 卡密 ----------
case 'card':
    $card_code = isset($input['card_code']) ? trim($input['card_code']) : (isset($_GET['card_code']) ? trim($_GET['card_code']) : '');
    if ($action === 'check') {
        if ($card_code === '') {
            apiJsonResp(400, '请输入卡密', ['valid' => false]);
            exit;
        }
        $stmt = $pdo->prepare('SELECT id, status, paused, expires_at FROM card_keys WHERE card_code = ? LIMIT 1');
        $stmt->execute([$card_code]);
        $row = $stmt->fetch();
        if (!$row) {
            apiJsonResp(0, '卡密不存在', ['valid' => false]);
            exit;
        }
        if (!empty($row['paused'])) {
            apiJsonResp(0, '卡密已暂停', ['valid' => false]);
            exit;
        }
        if ((int) $row['status'] === 1) {
            apiJsonResp(0, '该卡密已被使用', ['valid' => false]);
            exit;
        }
        if ($row['expires_at'] !== null && strtotime($row['expires_at']) < time()) {
            apiJsonResp(0, '卡密已过期', ['valid' => false]);
            exit;
        }
        apiJsonResp(0, '卡密有效', ['valid' => true]);
        exit;
    }
    requireLogin();
    $adminId = (int) ($_SESSION['admin_user_id'] ?? 0);
    $isAgent = (isset($_SESSION['admin_role']) && $_SESSION['admin_role'] === 'agent');
    switch ($action) {
        case 'list':
            try {
                $keyword = isset($input['keyword']) ? trim((string) $input['keyword']) : '';
                $status = isset($input['status']) ? $input['status'] : '';
                $paused = isset($input['paused']) ? $input['paused'] : '';
                $card_type = isset($input['card_type']) ? trim((string) $input['card_type']) : '';
                $page = max(1, (int) (isset($input['page']) ? $input['page'] : 1));
                $page_size = max(1, min(100, (int) (isset($input['page_size']) ? $input['page_size'] : 20)));
                $where = ['1=1'];
                $params = [];
                if ($isAgent) {
                    $where[] = 'c.creator_id = ?';
                    $params[] = $adminId;
                }
                if ($keyword !== '') {
                    $where[] = '(c.card_code LIKE ? OR c.remark LIKE ?)';
                    $params[] = '%' . $keyword . '%';
                    $params[] = '%' . $keyword . '%';
                }
                if ($status !== '' && $status !== 'all') {
                    $where[] = 'c.status = ?';
                    $params[] = (int) $status;
                }
                if ($paused !== '' && $paused !== 'all') {
                    $where[] = 'c.paused = ?';
                    $params[] = (int) $paused;
                }
                if ($card_type !== '' && $card_type !== 'all' && in_array($card_type, ['day', 'week', 'month'], true)) {
                    $where[] = 'c.card_type = ?';
                    $params[] = $card_type;
                }
                $whereSql = implode(' AND ', $where);
                $fromTable = 'card_keys c';
                if (!$isAgent) {
                    $fromTable .= ' LEFT JOIN admin_users au ON au.id = c.creator_id';
                }
                $stmt = $pdo->prepare("SELECT COUNT(*) FROM $fromTable WHERE $whereSql");
                $stmt->execute($params);
                $total = (int) $stmt->fetchColumn();
                $offset = ($page - 1) * $page_size;
                $sel = 'c.id, c.card_code, c.status, c.paused, c.expires_at, c.card_type, c.user_id, c.bound_ip, c.bound_at, c.remark, c.created_at, c.creator_id';
                if (!$isAgent) {
                    $sel .= ', au.username AS creator_username';
                }
                $sql = "SELECT $sel FROM $fromTable WHERE $whereSql ORDER BY c.id DESC LIMIT " . (int) $page_size . " OFFSET " . (int) $offset;
                $stmt = $pdo->prepare($sql);
                $stmt->execute($params);
                $list = $stmt->fetchAll();
                $typeNames = ['day' => '天卡', 'week' => '周卡', 'month' => '月卡'];
                $logStmt = $pdo->prepare('SELECT ip FROM user_login_log WHERE user_id = ? ORDER BY login_at DESC');
                foreach ($list as &$r) {
                    $r['status_text'] = (int) $r['status'] === 1 ? '已使用' : '未使用';
                    if ($r['expires_at'] && strtotime($r['expires_at']) < time()) $r['status_text'] = '已过期';
                    $r['paused'] = isset($r['paused']) ? (int) $r['paused'] : 0;
                    $r['card_type_text'] = isset($r['card_type'], $typeNames[$r['card_type']]) ? $typeNames[$r['card_type']] : (empty($r['expires_at']) ? '永久' : '自定义');
                    $r['bound_at'] = $r['bound_at'] ?: null;
                    $r['expires_at'] = $r['expires_at'] ?: null;
                    $r['login_ips'] = [];
                    if (!empty($r['user_id'])) {
                        $logStmt->execute([$r['user_id']]);
                        $seen = [];
                        while ($row = $logStmt->fetch()) {
                            $ip = $row['ip'];
                            if (!isset($seen[$ip])) { $seen[$ip] = true; $r['login_ips'][] = $ip; }
                        }
                    }
                }
                apiJsonResp(0, 'ok', ['list' => $list, 'total' => $total, 'page' => $page, 'page_size' => $page_size]);
            } catch (PDOException $e) {
                apiJsonResp(500, '卡密列表加载失败: ' . $e->getMessage());
            } catch (Throwable $e) {
                apiJsonResp(500, '卡密列表加载失败: ' . $e->getMessage());
            }
            break;
        case 'generate':
            $count = isset($input['count']) ? (int) $input['count'] : 1;
            $len = isset($input['length']) ? (int) $input['length'] : 16;
            $type = isset($input['type']) ? trim((string) $input['type']) : '';
            $count = max(1, min(100, $count));
            $len = max(8, min(32, $len));
            $expires_at = null;
            $card_type = null;
            if ($type === 'day') $card_type = 'day';
            elseif ($type === 'week') $card_type = 'week';
            elseif ($type === 'month') $card_type = 'month';
            $chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
            $generated = [];
            $creatorId = $isAgent ? $adminId : null;
            $insertStmt = $pdo->prepare('INSERT INTO card_keys (card_code, expires_at, card_type, creator_id) VALUES (?, ?, ?, ?)');
            for ($i = 0; $i < $count; $i++) {
                $code = '';
                for ($j = 0; $j < $len; $j++) $code .= $chars[random_int(0, strlen($chars) - 1)];
                try {
                    $insertStmt->execute([$code, $expires_at, $card_type, $creatorId]);
                    $generated[] = $code;
                } catch (PDOException $e) {
                    if (strpos($e->getMessage(), 'Duplicate') !== false) { $i--; continue; }
                    apiJsonResp(500, '生成失败: ' . $e->getMessage());
                    exit;
                }
            }
            $typeNames = ['day' => '天卡', 'week' => '周卡', 'month' => '月卡'];
            $typeText = isset($typeNames[$card_type]) ? $typeNames[$card_type] : '';
            adminLog($pdo, '生成卡密', '数量=' . count($generated) . ' 类型=' . ($card_type ?: '无'));
            apiJsonResp(0, '生成成功', ['keys' => $generated, 'count' => count($generated), 'card_type' => $card_type, 'card_type_text' => $typeText]);
            break;
        case 'update_remark':
            $id = isset($input['id']) ? (int) $input['id'] : 0;
            $remark = isset($input['remark']) ? trim((string) $input['remark']) : '';
            if ($id <= 0) {
                apiJsonResp(400, '请提供卡密 id');
                exit;
            }
            $remark = mb_substr($remark, 0, 255);
            $creatorCond = $isAgent ? ' AND creator_id = ' . (int) $adminId : '';
            $stmt = $pdo->prepare('UPDATE card_keys SET remark = ? WHERE id = ?' . $creatorCond);
            $stmt->execute([$remark === '' ? null : $remark, $id]);
            if ($stmt->rowCount() > 0) {
                adminLog($pdo, '编辑卡密备注', 'id=' . $id);
                apiJsonResp(0, '更新成功');
            } else {
                apiJsonResp(404, '记录不存在');
            }
            break;
        case 'delete':
            $ids = isset($input['ids']) && is_array($input['ids']) ? array_filter(array_map('intval', $input['ids'])) : [];
            $id = isset($input['id']) ? (int) $input['id'] : 0;
            $card_code_del = isset($input['card_code']) ? trim($input['card_code']) : '';
            $creatorCond = $isAgent ? ' AND creator_id = ' . (int) $adminId : '';
            if (!empty($ids)) {
                $placeholders = implode(',', array_fill(0, count($ids), '?'));
                $stmt = $pdo->prepare("DELETE FROM card_keys WHERE id IN ($placeholders)" . $creatorCond);
                $stmt->execute($ids);
                adminLog($pdo, '删除卡密', '数量=' . $stmt->rowCount());
                apiJsonResp(0, '删除成功', ['affected' => $stmt->rowCount()]);
            } elseif ($id > 0) {
                $stmt = $pdo->prepare('DELETE FROM card_keys WHERE id = ?' . $creatorCond);
                $stmt->execute([$id]);
                if ($stmt->rowCount() > 0) {
                    adminLog($pdo, '删除卡密', 'id=' . $id);
                    apiJsonResp(0, '删除成功');
                } else {
                    apiJsonResp(404, '记录不存在');
                }
            } elseif ($card_code_del !== '') {
                $stmt = $pdo->prepare('DELETE FROM card_keys WHERE card_code = ?' . $creatorCond);
                $stmt->execute([$card_code_del]);
                if ($stmt->rowCount() > 0) {
                    adminLog($pdo, '删除卡密', 'card_code=' . $card_code_del);
                    apiJsonResp(0, '删除成功');
                } else {
                    apiJsonResp(404, '记录不存在');
                }
            } else {
                apiJsonResp(400, '请提供 id、ids 或 card_code');
            }
            break;
        case 'pause':
            $ids = isset($input['ids']) && is_array($input['ids']) ? array_filter(array_map('intval', $input['ids'])) : [];
            $id = (int) (isset($input['id']) ? $input['id'] : (isset($_GET['id']) ? $_GET['id'] : 0));
            $creatorCond = $isAgent ? ' AND creator_id = ' . (int) $adminId : '';
            if (!empty($ids)) {
                $placeholders = implode(',', array_fill(0, count($ids), '?'));
                $stmt = $pdo->prepare("UPDATE card_keys SET paused = 1 WHERE id IN ($placeholders)" . $creatorCond);
                $stmt->execute($ids);
                adminLog($pdo, '暂停卡密', '数量=' . $stmt->rowCount());
                apiJsonResp(0, '已暂停', ['affected' => $stmt->rowCount()]);
            } elseif ($id > 0) {
                $stmt = $pdo->prepare('UPDATE card_keys SET paused = 1 WHERE id = ?' . $creatorCond);
                $stmt->execute([$id]);
                if ($stmt->rowCount() > 0) {
                    adminLog($pdo, '暂停卡密', 'id=' . $id);
                    apiJsonResp(0, '已暂停');
                } else {
                    $chk = $pdo->prepare('SELECT id FROM card_keys WHERE id = ? LIMIT 1');
                    $chk->execute([$id]);
                    if (!$chk->fetch()) {
                        apiJsonResp(404, '卡密不存在或已删除');
                    } else {
                        apiJsonResp(0, '已暂停');
                    }
                }
            } else {
                apiJsonResp(400, '请提供卡密 id 或 ids');
            }
            break;
        case 'enable':
            $ids = isset($input['ids']) && is_array($input['ids']) ? array_filter(array_map('intval', $input['ids'])) : [];
            $id = (int) (isset($input['id']) ? $input['id'] : (isset($_GET['id']) ? $_GET['id'] : 0));
            $creatorCond = $isAgent ? ' AND creator_id = ' . (int) $adminId : '';
            if (!empty($ids)) {
                $placeholders = implode(',', array_fill(0, count($ids), '?'));
                $stmt = $pdo->prepare("UPDATE card_keys SET paused = 0 WHERE id IN ($placeholders)" . $creatorCond);
                $stmt->execute($ids);
                adminLog($pdo, '启用卡密', '数量=' . $stmt->rowCount());
                apiJsonResp(0, '已启用', ['affected' => $stmt->rowCount()]);
            } elseif ($id > 0) {
                $stmt = $pdo->prepare('UPDATE card_keys SET paused = 0 WHERE id = ?' . $creatorCond);
                $stmt->execute([$id]);
                if ($stmt->rowCount() > 0) {
                    adminLog($pdo, '启用卡密', 'id=' . $id);
                    apiJsonResp(0, '已启用');
                } else {
                    $chk = $pdo->prepare('SELECT id FROM card_keys WHERE id = ? LIMIT 1');
                    $chk->execute([$id]);
                    if (!$chk->fetch()) {
                        apiJsonResp(404, '卡密不存在或已删除');
                    } else {
                        apiJsonResp(0, '已启用');
                    }
                }
            } else {
                apiJsonResp(400, '请提供卡密 id 或 ids');
            }
            break;
        case 'list_agent_cards':
            if ($isAgent) {
                apiJsonResp(403, '仅总管理可查看');
                exit;
            }
            $page = max(1, (int) (isset($input['page']) ? $input['page'] : 1));
            $page_size = max(1, min(100, (int) (isset($input['page_size']) ? $input['page_size'] : 20)));
            $stmt = $pdo->prepare("SELECT COUNT(*) FROM card_keys c WHERE c.creator_id IS NOT NULL");
            $stmt->execute();
            $total = (int) $stmt->fetchColumn();
            $offset = ($page - 1) * $page_size;
            $stmt = $pdo->prepare("SELECT c.id, c.card_code, c.status, c.paused, c.expires_at, c.card_type, c.user_id, c.bound_ip, c.bound_at, c.remark, c.created_at, c.creator_id, au.username AS creator_username FROM card_keys c LEFT JOIN admin_users au ON au.id = c.creator_id WHERE c.creator_id IS NOT NULL ORDER BY c.id DESC LIMIT " . (int) $page_size . " OFFSET " . (int) $offset);
            $stmt->execute();
            $list = $stmt->fetchAll();
            $typeNames = ['day' => '天卡', 'week' => '周卡', 'month' => '月卡'];
            $logStmt = $pdo->prepare('SELECT ip FROM user_login_log WHERE user_id = ? ORDER BY login_at DESC');
            foreach ($list as &$r) {
                $r['status_text'] = (int) $r['status'] === 1 ? '已使用' : '未使用';
                if ($r['expires_at'] && strtotime($r['expires_at']) < time()) $r['status_text'] = '已过期';
                $r['paused'] = isset($r['paused']) ? (int) $r['paused'] : 0;
                $r['card_type_text'] = isset($r['card_type'], $typeNames[$r['card_type']]) ? $typeNames[$r['card_type']] : (empty($r['expires_at']) ? '永久' : '自定义');
                $r['bound_at'] = $r['bound_at'] ?: null;
                $r['expires_at'] = $r['expires_at'] ?: null;
                $r['login_ips'] = [];
                if (!empty($r['user_id'])) {
                    $logStmt->execute([$r['user_id']]);
                    $seen = [];
                    while ($row = $logStmt->fetch()) {
                        $ip = $row['ip'];
                        if (!isset($seen[$ip])) { $seen[$ip] = true; $r['login_ips'][] = $ip; }
                    }
                }
                $r['creator_username'] = $r['creator_username'] ?? '-';
            }
            apiJsonResp(0, 'ok', ['list' => $list, 'total' => $total, 'page' => $page, 'page_size' => $page_size]);
            break;
        default:
            apiJsonResp(400, '无效的 action，支持: check | list | list_agent_cards | generate | update_remark | delete | pause | enable');
    }
    break;

// ---------- 后台用户管理 ----------
case 'admin_users':
    requireLogin();
    $adminId = (int) ($_SESSION['admin_user_id'] ?? 0);
    $isAgent = (isset($_SESSION['admin_role']) && $_SESSION['admin_role'] === 'agent');
    if ($action === 'list') {
        try {
            $keyword = isset($_GET['keyword']) ? trim((string) $_GET['keyword']) : (isset($input['keyword']) ? trim((string) $input['keyword']) : '');
            $page = max(1, (int) (isset($_GET['page']) ? $_GET['page'] : (isset($input['page']) ? $input['page'] : 1)));
            $page_size = max(1, min(100, (int) (isset($_GET['page_size']) ? $_GET['page_size'] : (isset($input['page_size']) ? $input['page_size'] : 20))));
            $where = ['1=1'];
            $params = [];
            // 代理只能查看「使用自己生成的卡密」的用户
            if ($isAgent) {
                $where[] = 'c.creator_id = ?';
                $params[] = $adminId;
            }
            if ($keyword !== '') {
                $where[] = 'u.username LIKE ?';
                $params[] = '%' . $keyword . '%';
            }
            $whereSql = implode(' AND ', $where);
            $fromUsers = 'users u LEFT JOIN card_keys c ON c.id = u.card_key_id';
            $stmt = $pdo->prepare("SELECT COUNT(*) FROM $fromUsers WHERE $whereSql");
            $stmt->execute($params);
            $total = (int) $stmt->fetchColumn();
            $offset = ($page - 1) * $page_size;
            $sql = "SELECT u.id, u.username, u.card_key_id, u.created_at, c.card_code, c.expires_at, c.paused AS card_paused, c.status AS card_status FROM $fromUsers WHERE $whereSql ORDER BY u.id DESC LIMIT " . (int) $page_size . " OFFSET " . (int) $offset;
            $stmt = $pdo->prepare($sql);
            $stmt->execute($params);
            $list = $stmt->fetchAll();
            $logStmt = $pdo->prepare('SELECT ip, login_at FROM user_login_log WHERE user_id = ? ORDER BY login_at DESC');
            // 在线房间信息：根据 IP 关联到 user_room_online
            try {
                $roomStmt = $pdo->prepare('SELECT room FROM user_room_online WHERE ip = ? ORDER BY updated_at DESC LIMIT 1');
            } catch (PDOException $e) {
                $roomStmt = null;
            }

            // 从端口日志服务获取最近一段时间内在 9999 端口有连接的 IP
            // 窗口时间 600 秒（10 分钟），可按需调整
            $portLoggerIps = [];
            try {
                $portLoggerIps = getRecentPortLoggerIps(9999, 600);
            } catch (Exception $e) {
                $portLoggerIps = [];
            }
            foreach ($list as &$r) {
                $r['login_ips'] = [];
                $r['last_login_at'] = null;
                $r['last_login_ip'] = null;
                $r['online'] = false;
                $r['room'] = null;
                $logStmt->execute([$r['id']]);
                $seen = [];
                $isFirst = true;
                while ($row = $logStmt->fetch()) {
                    $ip = $row['ip'];
                    if ($isFirst && !empty($row['login_at'])) {
                        $r['last_login_at'] = $row['login_at'];
                        $r['last_login_ip'] = $ip;
                        $isFirst = false;
                    }
                    if (!isset($seen[$ip])) { $seen[$ip] = true; $r['login_ips'][] = $ip; }
                }
                // 用户名 + IP 双因素 + 端口日志：需要最近一条登录记录，且该 IP 目前在 9999 端口有活跃连接
                if (!empty($r['last_login_at']) && !empty($r['last_login_ip'])) {
                    $ts = strtotime($r['last_login_at']);
                    if ($ts !== false && (time() - $ts) <= 3600) { // 最近 1 小时有登录
                        $ip = $r['last_login_ip'];
                        if ($ip && isset($portLoggerIps[$ip])) {
                            $r['online'] = true;
                            // 如果在线，尝试查询该 IP 当前所在的房间号
                            if ($roomStmt) {
                                try {
                                    $roomStmt->execute([$ip]);
                                    $roomRow = $roomStmt->fetch();
                                    if ($roomRow && isset($roomRow['room'])) {
                                        $r['room'] = $roomRow['room'];
                                    }
                                } catch (PDOException $e) {
                                    // 忽略房间查询错误，不影响主流程
                                }
                            }
                        }
                    }
                }
                $r['card_valid'] = true;
                if (empty($r['card_key_id'])) {
                    $r['card_code'] = '-';
                    $r['user_status'] = '未绑定';
                } else {
                    if (empty($r['card_code'])) {
                        $r['card_code'] = '(已删除)';
                        $r['card_valid'] = false;
                        $r['user_status'] = '已过期';
                    } elseif ($r['expires_at'] && strtotime($r['expires_at']) < time()) {
                        $r['card_valid'] = false;
                        $r['user_status'] = '已过期';
                    } else {
                        $r['user_status'] = !empty($r['card_paused']) ? '已暂停' : '正常';
                    }
                }
            }
            apiJsonResp(0, 'ok', ['list' => $list, 'total' => $total, 'page' => $page, 'page_size' => $page_size]);
        } catch (PDOException $e) {
            apiJsonResp(500, '用户列表加载失败: ' . $e->getMessage());
        } catch (Throwable $e) {
            apiJsonResp(500, '用户列表加载失败: ' . $e->getMessage());
        }
        exit;
    } elseif ($action === 'delete') {
        $id = isset($input['id']) ? (int) $input['id'] : 0;
        if ($id <= 0) {
            apiJsonResp(400, '请提供用户 ID');
            exit;
        }
        try {
            if ($isAgent) {
                $chk = $pdo->prepare('SELECT u.id FROM users u INNER JOIN card_keys c ON c.id = u.card_key_id WHERE u.id = ? AND c.creator_id = ? LIMIT 1');
                $chk->execute([$id, $adminId]);
                if (!$chk->fetch()) {
                    apiJsonResp(403, '只能删除使用自己卡密激活的用户');
                    exit;
                }
            }
            $pdo->prepare('DELETE FROM user_login_log WHERE user_id = ?')->execute([$id]);
            $stmt = $pdo->prepare('DELETE FROM users WHERE id = ?');
            $stmt->execute([$id]);
            if ($stmt->rowCount() > 0) {
                adminLog($pdo, '删除用户', 'id=' . $id);
                apiJsonResp(0, '删除成功');
            } else {
                apiJsonResp(404, '用户不存在');
            }
        } catch (PDOException $e) {
            apiJsonResp(500, '删除失败: ' . $e->getMessage());
        }
        exit;
    } elseif ($action === 'extend_expire') {
        $user_id = isset($input['user_id']) ? (int) $input['user_id'] : 0;
        $days = isset($input['days']) ? (int) $input['days'] : 0;
        if ($user_id <= 0) {
            apiJsonResp(400, '请提供用户 ID');
            exit;
        }
        if ($days <= 0) {
            apiJsonResp(400, '加时天数必须为正整数');
            exit;
        }
        try {
            // 查询用户及其绑定的卡密（代理只能给「自己卡密」的用户加时）
            $stmt = $pdo->prepare('SELECT u.username, u.card_key_id, c.expires_at, c.creator_id FROM users u LEFT JOIN card_keys c ON c.id = u.card_key_id WHERE u.id = ? LIMIT 1');
            $stmt->execute([$user_id]);
            $row = $stmt->fetch();
            if (!$row) {
                apiJsonResp(404, '用户不存在');
                exit;
            }
            if (empty($row['card_key_id'])) {
                apiJsonResp(400, '该用户尚未绑定卡密，无法加时');
                exit;
            }
            if ($isAgent && (int) $row['creator_id'] !== $adminId) {
                apiJsonResp(403, '只能对使用自己卡密激活的用户加时');
                exit;
            }
            $baseTime = null;
            if (!empty($row['expires_at']) && strtotime($row['expires_at']) !== false) {
                $baseTime = strtotime($row['expires_at']);
            }
            $now = time();
            if ($baseTime === null || $baseTime < $now) {
                $baseTime = $now;
            }
            $newExpiresAt = date('Y-m-d H:i:s', $baseTime + $days * 86400);
            $upd = $pdo->prepare('UPDATE card_keys SET expires_at = ? WHERE id = ?');
            $upd->execute([$newExpiresAt, (int) $row['card_key_id']]);
            if ($upd->rowCount() <= 0) {
                // 如果受影响行数为 0，有两种可能：
                // 1）记录不存在  2）新旧到期时间相同
                // 这里再检查一次记录是否存在，不存在才视为错误，存在则视为加时成功（只是时间未变化）
                $chk = $pdo->prepare('SELECT id FROM card_keys WHERE id = ? LIMIT 1');
                $chk->execute([(int) $row['card_key_id']]);
                if (!$chk->fetch()) {
                    apiJsonResp(404, '卡密不存在或已删除');
                    exit;
                }
                // 记录存在但未修改，继续按成功处理
            }
            // 记录操作日志：使用用户名 + 加多少天
            $logUsername = isset($row['username']) ? (string)$row['username'] : ('ID=' . $user_id);
            adminLog($pdo, '用户加时', '用户名=' . $logUsername . ' 天数=' . $days);
            apiJsonResp(0, '加时成功', ['new_expires_at' => $newExpiresAt]);
        } catch (PDOException $e) {
            apiJsonResp(500, '加时失败: ' . $e->getMessage());
        }
        exit;
    } else {
        apiJsonResp(400, '无效的 action，支持: list | delete | extend_expire');
        exit;
    }
    break;

// ---------- 数据统计 ----------
case 'stats':
    requireLogin();
    $adminId = (int) ($_SESSION['admin_user_id'] ?? 0);
    $isAgent = (isset($_SESSION['admin_role']) && $_SESSION['admin_role'] === 'agent');
    $cardWhere = $isAgent ? ' WHERE creator_id = ' . $adminId : '';
    $cardWhereAnd = $isAgent ? ' AND creator_id = ' . $adminId : '';
    $now = date('Y-m-d H:i:s');
    $todayStart = date('Y-m-d 00:00:00');
    $weekStart = date('Y-m-d 00:00:00', strtotime('-6 days'));
    $total_users = (int) $pdo->query('SELECT COUNT(*) FROM users')->fetchColumn();
    $total_cards = (int) $pdo->query('SELECT COUNT(*) FROM card_keys' . $cardWhere)->fetchColumn();
    $cards_unused = (int) $pdo->query('SELECT COUNT(*) FROM card_keys WHERE status = 0' . $cardWhereAnd)->fetchColumn();
    $cards_used = (int) $pdo->query('SELECT COUNT(*) FROM card_keys WHERE status = 1' . $cardWhereAnd)->fetchColumn();
    $stmt = $pdo->prepare('SELECT COUNT(*) FROM card_keys WHERE status = 1 AND expires_at IS NOT NULL AND expires_at < ?' . $cardWhereAnd);
    $stmt->execute([$now]);
    $cards_expired = (int) $stmt->fetchColumn();
    $stmt = $pdo->prepare('SELECT COUNT(*) FROM users WHERE created_at >= ?');
    $stmt->execute([$todayStart]);
    $today_users = (int) $stmt->fetchColumn();
    $stmt = $pdo->prepare('SELECT COUNT(*) FROM users WHERE created_at >= ?');
    $stmt->execute([$weekStart]);
    $week_users = (int) $stmt->fetchColumn();
    $stmt = $pdo->prepare('SELECT COUNT(*) FROM card_keys WHERE status = 1 AND bound_at >= ?' . $cardWhereAnd);
    $stmt->execute([$todayStart]);
    $today_activations = (int) $stmt->fetchColumn();
    $stmt = $pdo->prepare('SELECT COUNT(*) FROM card_keys WHERE status = 1 AND bound_at >= ?' . $cardWhereAnd);
    $stmt->execute([$weekStart]);
    $week_activations = (int) $stmt->fetchColumn();
    apiJsonResp(0, 'ok', [
        'total_users' => $total_users,
        'total_cards' => $total_cards,
        'cards_unused' => $cards_unused,
        'cards_used' => $cards_used,
        'cards_expired' => $cards_expired,
        'today_users' => $today_users,
        'week_users' => $week_users,
        'today_activations' => $today_activations,
        'week_activations' => $week_activations,
    ]);
    break;

// ---------- 黑名单（仅总管理） ----------
case 'blacklist':
    requireLogin();
    if (isAgent()) {
        apiJsonResp(403, '仅总管理可操作黑名单');
        exit;
    }
    $hasTable = false;
    try {
        $pdo->query('SELECT 1 FROM blacklist LIMIT 1');
        $hasTable = true;
    } catch (PDOException $e) {}
    if (!$hasTable) {
        if ($action === 'list') {
            apiJsonResp(0, 'ok', ['list' => []]);
        } else {
            apiJsonResp(500, '黑名单功能未就绪，请先执行 auth/upgrade_blacklist.sql');
        }
        exit;
    }
    if ($action === 'list') {
        $stmt = $pdo->query('SELECT id, type, value, created_at FROM blacklist ORDER BY type ASC, id DESC');
        apiJsonResp(0, 'ok', ['list' => $stmt->fetchAll()]);
        exit;
    }
    if ($action === 'add') {
        $type = isset($input['type']) ? trim((string) $input['type']) : '';
        $value = isset($input['value']) ? trim((string) $input['value']) : '';
        if (!in_array($type, ['ip', 'user'], true)) {
            apiJsonResp(400, '类型须为 ip 或 user');
            exit;
        }
        if ($value === '') {
            apiJsonResp(400, $type === 'ip' ? '请输入IP' : '请输入用户名');
            exit;
        }
        try {
            $stmt = $pdo->prepare('INSERT INTO blacklist (type, value) VALUES (?, ?)');
            $stmt->execute([$type, $value]);
            adminLog($pdo, '添加黑名单', $type . '=' . $value);
            
            // 如果是IP类型，同时调用封禁IP功能
            if ($type === 'ip') {
                $blockResult = blockIPViaPortLogger($value, '黑名单封禁');
                if ($blockResult['success']) {
                    apiJsonResp(0, '添加成功并已封禁IP', ['id' => (int) $pdo->lastInsertId(), 'blocked' => true]);
                } else {
                    // 黑名单已添加，但封禁可能失败（不影响黑名单功能）
                    apiJsonResp(0, '添加成功（封禁IP失败: ' . $blockResult['error'] . '）', ['id' => (int) $pdo->lastInsertId(), 'blocked' => false, 'block_error' => $blockResult['error']]);
                }
            } else {
                apiJsonResp(0, '添加成功', ['id' => (int) $pdo->lastInsertId()]);
            }
        } catch (PDOException $e) {
            if (strpos($e->getMessage(), 'Duplicate') !== false) {
                apiJsonResp(400, '已存在相同记录');
            } else {
                apiJsonResp(500, '添加失败: ' . $e->getMessage());
            }
        }
        exit;
    }
    if ($action === 'delete') {
        $id = isset($input['id']) ? (int) $input['id'] : 0;
        if ($id <= 0) {
            apiJsonResp(400, '请提供 id');
            exit;
        }
        try {
            // 先查询记录信息
            $stmt = $pdo->prepare('SELECT type, value FROM blacklist WHERE id = ?');
            $stmt->execute([$id]);
            $row = $stmt->fetch();
            if (!$row) {
                apiJsonResp(404, '记录不存在');
                exit;
            }
            
            // 删除黑名单记录
            $stmt = $pdo->prepare('DELETE FROM blacklist WHERE id = ?');
            $stmt->execute([$id]);
            adminLog($pdo, '删除黑名单', $row['type'] . '=' . $row['value']);
            
            // 如果是IP类型，同时尝试解封该IP
            if ($row['type'] === 'ip') {
                $unblockResult = unblockIPViaPortLogger($row['value']);
                if ($unblockResult['success']) {
                    apiJsonResp(0, '删除成功并已解封IP', ['unblocked' => true]);
                } else {
                    // 黑名单已删除，但解封可能失败（不影响黑名单功能）
                    apiJsonResp(0, '删除成功（解封IP失败: ' . $unblockResult['error'] . '）', ['unblocked' => false, 'unblock_error' => $unblockResult['error']]);
                }
            } else {
                apiJsonResp(0, '删除成功');
            }
        } catch (PDOException $e) {
            apiJsonResp(500, '删除失败: ' . $e->getMessage());
        }
        exit;
    }
    apiJsonResp(400, '无效的 action，支持: list | add | delete');
    break;

// ---------- 代理管理（仅总管理） ----------
case 'agents':
    requireLogin();
    if (isset($_SESSION['admin_role']) && $_SESSION['admin_role'] === 'agent') {
        apiJsonResp(403, '仅总管理可管理代理');
        exit;
    }
    if ($action === 'list') {
        try {
            $stmt = $pdo->query("SELECT id, username, created_at FROM admin_users WHERE role = 'agent' ORDER BY id ASC");
            $list = $stmt->fetchAll();
        } catch (PDOException $e) {
            if (strpos($e->getMessage(), 'role') !== false) {
                apiJsonResp(0, 'ok', ['list' => []]);
                exit;
            }
            throw $e;
        }
        apiJsonResp(0, 'ok', ['list' => $list]);
        exit;
    }
    if ($action === 'add') {
        $username = isset($input['username']) ? trim((string) $input['username']) : '';
        $password = isset($input['password']) ? (string) $input['password'] : '';
        if ($username === '' || mb_strlen($username) < 2) {
            apiJsonResp(400, '用户名至少2位');
            exit;
        }
        if (strlen($password) < 4) {
            apiJsonResp(400, '密码至少4位');
            exit;
        }
        $hash = password_hash($password, PASSWORD_DEFAULT);
        try {
            $stmt = $pdo->prepare("INSERT INTO admin_users (username, password, role) VALUES (?, ?, 'agent')");
            $stmt->execute([$username, $hash]);
            adminLog($pdo, '添加代理', 'username=' . $username);
            apiJsonResp(0, '添加成功', ['id' => (int) $pdo->lastInsertId(), 'username' => $username]);
        } catch (PDOException $e) {
            if (strpos($e->getMessage(), 'Duplicate') !== false) {
                apiJsonResp(400, '该用户名已存在');
            } else {
                apiJsonResp(500, '添加失败: ' . $e->getMessage());
            }
        }
        exit;
    }
    if ($action === 'delete') {
        $id = isset($input['id']) ? (int) $input['id'] : 0;
        if ($id <= 0) {
            apiJsonResp(400, '请提供代理 ID');
            exit;
        }
        $stmt = $pdo->prepare("SELECT id, username, current_session_id FROM admin_users WHERE id = ? AND role = 'agent' LIMIT 1");
        $stmt->execute([$id]);
        $agent = $stmt->fetch();
        if (!$agent) {
            apiJsonResp(404, '代理不存在或已删除');
            exit;
        }
        $agent_session_id = isset($agent['current_session_id']) ? trim((string) $agent['current_session_id']) : '';
        try {
            // 1) 删除使用该代理卡密的用户及其登录日志
            $stmt = $pdo->prepare('SELECT u.id FROM users u INNER JOIN card_keys c ON c.id = u.card_key_id AND c.creator_id = ?');
            $stmt->execute([$id]);
            $userIds = $stmt->fetchAll(PDO::FETCH_COLUMN);
            $deletedUsers = 0;
            if (!empty($userIds)) {
                $placeholders = implode(',', array_fill(0, count($userIds), '?'));
                $pdo->prepare('DELETE FROM user_login_log WHERE user_id IN (' . $placeholders . ')')->execute($userIds);
                $delUsers = $pdo->prepare('DELETE FROM users WHERE id IN (' . $placeholders . ')');
                $delUsers->execute($userIds);
                $deletedUsers = $delUsers->rowCount();
            }
            // 2) 删除该代理生成的全部卡密
            $delCards = $pdo->prepare('DELETE FROM card_keys WHERE creator_id = ?');
            $delCards->execute([$id]);
            $deletedCards = $delCards->rowCount();
            // 3) 删除代理账号并踢下线
            $stmt = $pdo->prepare("DELETE FROM admin_users WHERE id = ? AND role = 'agent'");
            $stmt->execute([$id]);
            if ($stmt->rowCount() > 0) {
                if ($agent_session_id !== '') destroyAdminSessionById($agent_session_id);
                adminLog($pdo, '删除代理', 'username=' . $agent['username'] . ' 已删除其卡密 ' . $deletedCards . ' 条、对应用户 ' . $deletedUsers . ' 个');
                apiJsonResp(0, '删除成功', ['deleted_cards' => $deletedCards, 'deleted_users' => $deletedUsers]);
            } else {
                apiJsonResp(404, '代理不存在或已删除');
            }
        } catch (PDOException $e) {
            apiJsonResp(500, '删除失败: ' . $e->getMessage());
        }
        exit;
    }
    apiJsonResp(400, '无效的 action，支持: list | add | delete');
    break;

// ---------- 操作日志（仅总管理） ----------
case 'operation_log':
    requireLogin();
    if (isAgent()) {
        apiJsonResp(403, '仅总管理可查看操作日志');
        exit;
    }
    $page = max(1, (int) (isset($_GET['page']) ? $_GET['page'] : 1));
    $page_size = max(1, min(100, (int) (isset($_GET['page_size']) ? $_GET['page_size'] : 30)));
    $hasTable = false;
    try {
        $pdo->query('SELECT 1 FROM admin_operation_log LIMIT 1');
        $hasTable = true;
    } catch (PDOException $e) {}
    if (!$hasTable) {
        apiJsonResp(0, 'ok', ['list' => [], 'total' => 0, 'page' => $page, 'page_size' => $page_size]);
        exit;
    }
    if ($action !== 'list') {
        apiJsonResp(400, '无效的 action，支持: list');
        exit;
    }
    $total = (int) $pdo->query('SELECT COUNT(*) FROM admin_operation_log')->fetchColumn();
    $offset = ($page - 1) * $page_size;
    $stmt = $pdo->prepare('SELECT id, admin_username, action, detail, created_at FROM admin_operation_log ORDER BY id DESC LIMIT ' . (int) $page_size . ' OFFSET ' . (int) $offset);
    $stmt->execute();
    apiJsonResp(0, 'ok', ['list' => $stmt->fetchAll(), 'total' => $total, 'page' => $page, 'page_size' => $page_size]);
    break;

// ---------- 导出 CSV ----------
case 'export':
    requireLogin();
    $adminId = (int) ($_SESSION['admin_user_id'] ?? 0);
    $isAgent = (isset($_SESSION['admin_role']) && $_SESSION['admin_role'] === 'agent');
    $type = isset($_GET['type']) ? trim($_GET['type']) : '';
    if (!in_array($type, ['cards', 'users'], true)) {
        apiJsonResp(400, 'type 须为 cards 或 users');
        exit;
    }
    $input = $_GET;
    $keyword = isset($input['keyword']) ? trim((string) $input['keyword']) : '';
    $limit = 5000;
    if ($type === 'cards') {
        $where = ['1=1'];
        $params = [];
        if ($isAgent) {
            $where[] = 'creator_id = ?';
            $params[] = $adminId;
        }
        if ($keyword !== '') {
            $where[] = '(card_code LIKE ? OR remark LIKE ?)';
            $params[] = '%' . $keyword . '%';
            $params[] = '%' . $keyword . '%';
        }
        $status = isset($input['status']) ? $input['status'] : '';
        $paused = isset($input['paused']) ? $input['paused'] : '';
        $card_type = isset($input['card_type']) ? trim((string) $input['card_type']) : '';
        if ($status !== '' && $status !== 'all') { $where[] = 'status = ?'; $params[] = (int) $status; }
        if ($paused !== '' && $paused !== 'all') { $where[] = 'paused = ?'; $params[] = (int) $paused; }
        if ($card_type !== '' && $card_type !== 'all' && in_array($card_type, ['day', 'week', 'month'], true)) {
            $where[] = 'card_type = ?'; $params[] = $card_type;
        }
        $whereSql = implode(' AND ', $where);
        $sql = "SELECT id, card_code, status, paused, expires_at, card_type, user_id, bound_ip, bound_at, remark, created_at FROM card_keys WHERE $whereSql ORDER BY id DESC LIMIT " . (int) $limit;
        $stmt = $pdo->prepare($sql);
        $stmt->execute($params);
        $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
        $typeNames = ['day' => '天卡', 'week' => '周卡', 'month' => '月卡'];
        if (ob_get_length()) ob_end_clean();
        header('Content-Type: text/csv; charset=UTF-8');
        header('Content-Disposition: attachment; filename="card_keys_' . date('YmdHis') . '.csv"');
        $out = fopen('php://output', 'w');
        fprintf($out, chr(0xEF).chr(0xBB).chr(0xBF));
        fputcsv($out, ['ID', '卡密', '类型', '状态', '暂停', '到期时间', '绑定用户ID', '注册IP', '使用时间', '备注', '创建时间']);
        foreach ($rows as $r) {
            $statusText = (int) $r['status'] === 1 ? '已使用' : '未使用';
            if ($r['expires_at'] && strtotime($r['expires_at']) < time()) $statusText = '已过期';
            $typeText = isset($r['card_type'], $typeNames[$r['card_type']]) ? $typeNames[$r['card_type']] : '';
            fputcsv($out, [$r['id'], $r['card_code'], $typeText, $statusText, (int) $r['paused'] ? '已暂停' : '启用', $r['expires_at'] ?: '', $r['user_id'] ?: '', $r['bound_ip'] ?: '', $r['bound_at'] ?: '', $r['remark'] ?: '', $r['created_at'] ?: '']);
        }
        fclose($out);
        exit;
    }
    $where = ['1=1'];
    $params = [];
    if ($keyword !== '') {
        $where[] = 'u.username LIKE ?';
        $params[] = '%' . $keyword . '%';
    }
    $whereSql = implode(' AND ', $where);
    $sql = "SELECT u.id, u.username, u.card_key_id, u.created_at, c.card_code, c.expires_at, c.paused AS card_paused FROM users u LEFT JOIN card_keys c ON c.id = u.card_key_id WHERE $whereSql ORDER BY u.id DESC LIMIT " . (int) $limit;
    $stmt = $pdo->prepare($sql);
    $stmt->execute($params);
    $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
    if (ob_get_length()) ob_end_clean();
    header('Content-Type: text/csv; charset=UTF-8');
    header('Content-Disposition: attachment; filename="users_' . date('YmdHis') . '.csv"');
    $out = fopen('php://output', 'w');
    fprintf($out, chr(0xEF).chr(0xBB).chr(0xBF));
    fputcsv($out, ['ID', '用户名', '卡密', '卡密状态', '到期时间', '注册时间']);
    foreach ($rows as $r) {
        $cardValid = true;
        if (!empty($r['card_key_id'])) {
            if (empty($r['card_code'])) $cardValid = false;
            elseif ($r['expires_at'] && strtotime($r['expires_at']) < time()) $cardValid = false;
        }
        $cardStatus = $cardValid ? '有效' : '已删除/已过期';
        fputcsv($out, [$r['id'], $r['username'], $r['card_code'] ?: '-', $cardStatus, $r['expires_at'] ?: '', $r['created_at'] ?: '']);
    }
    fclose($out);
    exit;

default:
    apiJsonResp(400, 'Unknown API module: ' . API_MODULE);
}
