<?php
/**
 * 知岁OS 数据对接接口
 *
 * 加密方式：AES-256-GCM
 * 流程：
 * 1. 从数据库读取服务商UUID和密钥配置
 * 2. 用 UUID+用户密钥 拼接后加密
 * 3. 请求知岁OS API获取数据
 * 4. 解密响应数据返回给前端或同步入库
 *
 * 支持接口：
 * - import_schedule: 课表导入（返回标准化JSON给前端）
 * - import_grades:   成绩导入（返回标准化JSON给前端）
 * - sync:            数据同步入库（type=course/score/userInfo等）
 * - test_connection: 连接测试（后台用）
 */

// API路径强制关闭错误输出，确保返回纯JSON
ini_set('display_errors', 0);
error_reporting(E_ERROR);

// CORS
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
    header("Access-Control-Allow-Origin: *");
    header("Access-Control-Allow-Methods: GET, POST, OPTIONS");
    header("Access-Control-Allow-Headers: Content-Type, Authorization, token, userId");
    http_response_code(204);
    exit;
}
header("Access-Control-Allow-Origin: *");
header("Content-Type: application/json; charset=utf-8");

// 启动会话
if (session_status() === PHP_SESSION_NONE) {
    session_start();
}

require_once __DIR__ . '/../config.php';

// 获取知岁OS服务商配置（从数据库settings表读取）
function getZsosConfig($db) {
    static $cfg = null;
    if ($cfg !== null) return $cfg;

    $cfg = ['base_url' => '', 'uuid' => '', 'secret_key' => ''];

    // 从 settings 表读取（group_name = 'zsos')
    $stmt = $db->prepare("SELECT key_name, value FROM settings WHERE group_name = 'zsos'");
    if ($stmt) {
        $stmt->execute();
        while ($row = $stmt->fetch()) {
            if ($row['key_name'] === 'zsos_base_url') $cfg['base_url'] = trim($row['value']);
            if ($row['key_name'] === 'zsos_uuid')    $cfg['uuid'] = trim($row['value']);
            if ($row['key_name'] === 'zsos_secret_key') $cfg['secret_key'] = trim($row['value']);
        }
    }

    // 降级使用 config.php 常量
    if (empty($cfg['base_url'])) $cfg['base_url'] = defined('ZSOS_BASE_URL') ? ZSOS_BASE_URL : 'https://yun.sx-wl.cn';
    if (empty($cfg['uuid']))    $cfg['uuid'] = defined('ZSOS_UUID') ? ZSOS_UUID : '';
    if (empty($cfg['secret_key'])) $cfg['secret_key'] = defined('ZSOS_SECRET_KEY') ? ZSOS_SECRET_KEY : '';

    return $cfg;
}

/**
 * AES-256-GCM 加密
 * @param string $data 待加密数据
 * @param string $key  密钥（二进制）
 * @return string base64(iv + tag + ciphertext)
 */
function aes256gcmEncrypt($data, $key) {
    $method = 'aes-256-gcm';
    $ivLen = openssl_cipher_iv_length($method);
    $iv = openssl_random_pseudo_bytes($ivLen);
    $tag = '';
    $encrypted = openssl_encrypt($data, $method, $key, OPENSSL_RAW_DATA, $iv, $tag);
    if ($encrypted === false) {
        return false;
    }
    return base64_encode($iv . $tag . $encrypted);
}

/**
 * AES-256-GCM 解密
 * @param string $packet base64编码的加密包 (iv + tag + ciphertext)
 * @param string $key    密钥（二进制）
 * @return string|false 解密后的明文
 */
function aes256gcmDecrypt($packet, $key) {
    $method = 'aes-256-gcm';
    $ivLen = openssl_cipher_iv_length($method);

    $raw = base64_decode($packet);
    if ($raw === false) return false;

    $iv = substr($raw, 0, $ivLen);
    $tag = substr($raw, $ivLen, 16);
    $ciphertext = substr($raw, $ivLen + 16);

    $decrypted = openssl_decrypt($ciphertext, $method, $key, OPENSSL_RAW_DATA, $iv, $tag);
    return $decrypted;
}

/**
 * 请求知岁OS API
 * @param string $uuid        服务商UUID
 * @param string $token       用户密钥
 * @param string $secretKeyHex 服务商密钥(hex格式)
 * @param string $baseUrl     API基础地址
 * @return array ['code' => int, 'data' => mixed, 'msg' => string]
 */
function fetchZsosData($uuid, $token, $secretKeyHex, $baseUrl) {
    // 将hex密钥转为二进制
    $secretKey = @hex2bin($secretKeyHex);
    if ($secretKey === false) {
        return ['code' => -1, 'data' => null, 'msg' => '服务商密钥格式错误'];
    }

    // 拼接加密数据：UUID + Token
    $plainData = $uuid . $token;

    // AES-256-GCM 加密
    $encryptedData = aes256gcmEncrypt($plainData, $secretKey);
    if ($encryptedData === false) {
        return ['code' => -1, 'data' => null, 'msg' => '数据加密失败'];
    }

    // 请求知岁OS API
    $apiUrl = rtrim($baseUrl, '/') . '/api/open/useToken';
    $postData = http_build_query([
        'uuid' => $uuid,
        'data' => $encryptedData,
    ]);

    $ch = curl_init();
    curl_setopt_array($ch, [
        CURLOPT_URL            => $apiUrl,
        CURLOPT_POST           => true,
        CURLOPT_POSTFIELDS     => $postData,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT        => 30,
        CURLOPT_SSL_VERIFYPEER => false,
        CURLOPT_SSL_VERIFYHOST => false,
        CURLOPT_HTTPHEADER     => [
            'Content-Type: application/x-www-form-urlencoded',
        ],
    ]);

    $response = curl_exec($ch);
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    $curlErr = curl_error($ch);
    curl_close($ch);

    if ($curlErr) {
        return ['code' => -1, 'data' => null, 'msg' => '请求知岁OS失败: ' . $curlErr];
    }

    if ($httpCode !== 200) {
        // 203 = 院校未授权，给出友好提示
        if ($httpCode === 203) {
            return ['code' => -1, 'data' => null, 'msg' => '平台并未获得该院校的授权，暂不支持此学校的数据同步'];
        }
        // 其他异常状态码
        $knownCodes = [
            401 => '密钥无效或已过期',
            403 => '无权限访问此数据',
            404 => '数据不存在',
            429 => '请求过于频繁，请稍后再试',
        ];
        $friendlyMsg = $knownCodes[$httpCode] ?? '知岁OS返回异常状态码: ' . $httpCode;
        return ['code' => -1, 'data' => null, 'msg' => $friendlyMsg];
    }

    // 解析响应
    $res = json_decode($response, true);
    if (!is_array($res)) {
        return ['code' => -1, 'data' => null, 'msg' => '知岁OS响应解析失败'];
    }

    if (!isset($res['code']) || $res['code'] != 1) {
        return ['code' => -1, 'data' => null, 'msg' => $res['msg'] ?? '知岁OS返回错误'];
    }

    // 解密响应数据
    if (empty($res['data'])) {
        return ['code' => -1, 'data' => null, 'msg' => '知岁OS返回数据为空'];
    }

    $decrypted = aes256gcmDecrypt($res['data'], $secretKey);
    if ($decrypted === false) {
        return ['code' => -1, 'data' => null, 'msg' => '数据解密失败，请检查服务商密钥是否正确'];
    }

    $data = json_decode($decrypted, true);
    if (!is_array($data)) {
        return ['code' => -1, 'data' => null, 'msg' => '解密数据解析失败'];
    }

    return ['code' => 0, 'data' => $data, 'msg' => 'success'];
}

// 输出JSON并退出
function outputJson($data) {
    echo json_encode($data, JSON_UNESCAPED_UNICODE);
    exit;
}

// ═══════════════════════════════════════════════
// 获取公共参数
// ═══════════════════════════════════════════════
$action = trim($_REQUEST['action'] ?? '');
$token  = trim((string)(($_REQUEST['key'] ?? '') ?: ($_REQUEST['token'] ?? '')));
$type   = trim($_REQUEST['type'] ?? '');

// 确保数据库连接
$db = getDB();

// 确保用户ID（自动登录）
if (!isset($_SESSION['user_id'])) {
    $_SESSION['user_id'] = 1;
}
$userId = $_SESSION['user_id'];

// ═══════════════════════════════════════════════
// 接口：导入课表数据（返回标准化JSON给前端）
// ═══════════════════════════════════════════════
if ($action === 'import_schedule') {
    if (empty($token)) {
        outputJson(['code' => -1, 'msg' => '请输入用户密钥', 'data' => []]);
    }

    $cfg = getZsosConfig($db);

    if (empty($cfg['uuid']) || empty($cfg['secret_key'])) {
        outputJson(['code' => -1, 'msg' => '管理员尚未配置知岁OS服务商信息，请联系管理员', 'data' => []]);
    }

    $result = fetchZsosData($cfg['uuid'], $token, $cfg['secret_key'], $cfg['base_url']);

    if ($result['code'] !== 0) {
        outputJson(['code' => -1, 'msg' => $result['msg'], 'data' => []]);
    }

    $osData = $result['data'];

    // 解析课表数据 — 智能检测 progress.courses 嵌套结构 vs 扁平课表数组
    $courses = [];
    $scheduleFound = false;

    $candidates = [];
    if (isset($osData['course']) && is_array($osData['course']) && !empty($osData['course'])) {
        $candidates[] = ['key' => 'course', 'value' => $osData['course']];
    }
    if (isset($osData['schedule']) && is_array($osData['schedule']) && !empty($osData['schedule'])) {
        $candidates[] = ['key' => 'schedule', 'value' => $osData['schedule']];
    }
    if (isset($osData['courses']) && is_array($osData['courses']) && !empty($osData['courses'])) {
        $candidates[] = ['key' => 'courses', 'value' => $osData['courses']];
    }
    if (isset($osData['data']) && is_array($osData['data']) && !empty($osData['data'])) {
        if (isset($osData['data'][0]) && is_array($osData['data'][0])) {
            $candidates[] = ['key' => 'data[]', 'value' => $osData['data']];
        } else {
            $innerData = $osData['data'];
            if (isset($innerData['course']) && is_array($innerData['course']) && !empty($innerData['course'])) {
                $candidates[] = ['key' => 'data.course', 'value' => $innerData['course']];
            }
            if (isset($innerData['courses']) && is_array($innerData['courses']) && !empty($innerData['courses'])) {
                $candidates[] = ['key' => 'data.courses', 'value' => $innerData['courses']];
            }
            if (isset($innerData['schedule']) && is_array($innerData['schedule']) && !empty($innerData['schedule'])) {
                $candidates[] = ['key' => 'data.schedule', 'value' => $innerData['schedule']];
            }
        }
    }
    if (isset($osData[0]) && is_array($osData[0])) {
        $first = $osData[0];
        if (isset($first['name']) || isset($first['courseName']) || isset($first['course_name']) ||
            isset($first['start']) || isset($first['week'])) {
            $candidates[] = ['key' => 'root[]', 'value' => $osData];
        }
    }

    // 遍历候选，优先选择扁平课表数组（含 start/end/week 字段）
    foreach ($candidates as $c) {
        $val = $c['value'];
        if (isProgressCoursesStructure($val)) {
            $flat = flattenProgressCourses($val);
            $hasScheduleFields = false;
            foreach ($flat as $item) {
                if (isset($item['start']) || isset($item['end']) || isset($item['week']) || isset($item['weeks'])) {
                    $hasScheduleFields = true;
                    break;
                }
            }
            if ($hasScheduleFields) {
                $courses = $flat;
                $scheduleFound = true;
                break;
            }
            continue;
        }
        $firstItem = null;
        foreach ($val as $item) {
            if (is_array($item)) { $firstItem = $item; break; }
        }
        if ($firstItem !== null && (
            isset($firstItem['start']) || isset($firstItem['end']) ||
            isset($firstItem['week']) || isset($firstItem['weeks']) ||
            isset($firstItem['startSection']) || isset($firstItem['weekday'])
        )) {
            $courses = $val;
            $scheduleFound = true;
            break;
        }
    }

    // 兜底：如果没有找到课表格式，使用扁平化后的 progress.courses
    if (!$scheduleFound) {
        foreach ($candidates as $c) {
            if (isProgressCoursesStructure($c['value'])) {
                $courses = flattenProgressCourses($c['value']);
                $scheduleFound = true;
                break;
            }
        }
    }
    if (!$scheduleFound && !empty($candidates)) {
        $courses = $candidates[0]['value'];
    }

    // 标准化课表字段
    $normalizedCourses = [];
    foreach ($courses as $c) {
        $weekday = intval($c['week'] ?? $c['weekday'] ?? $c['dayOfWeek'] ?? $c['day'] ?? 1);
        $startSection = intval($c['start'] ?? $c['startSection'] ?? $c['start_section'] ?? $c['sectionStart'] ?? 1);
        $endSection = intval($c['end'] ?? $c['endSection'] ?? $c['end_section'] ?? $c['sectionEnd'] ?? 1);
        $location = $c['floor'] ?? $c['location'] ?? $c['address'] ?? $c['place'] ?? '';

        // 处理周次
        $startWeek = 1;
        $endWeek = 16;
        if (isset($c['weeks']) && is_array($c['weeks']) && count($c['weeks']) > 0) {
            $weeksArr = array_map('intval', $c['weeks']);
            sort($weeksArr);
            $startWeek = $weeksArr[0];
            $endWeek = $weeksArr[count($weeksArr) - 1];
        } else {
            $startWeek = intval($c['startWeek'] ?? $c['start_week'] ?? 1);
            $endWeek = intval($c['endWeek'] ?? $c['end_week'] ?? 16);
        }

        $normalizedCourses[] = [
            'name'         => $c['name'] ?? $c['courseName'] ?? $c['course_name'] ?? '未知课程',
            'location'     => $location,
            'teacher'      => $c['teacher'] ?? $c['teacherName'] ?? $c['teacher_name'] ?? '',
            'weekday'      => $weekday,
            'startSection' => $startSection,
            'endSection'   => $endSection,
            'startWeek'    => $startWeek,
            'endWeek'      => $endWeek,
        ];
    }

    outputJson([
        'code' => 0,
        'msg'  => 'success',
        'data' => ['courses' => $normalizedCourses]
    ]);
}

// ═══════════════════════════════════════════════
// 接口：导入成绩数据（返回标准化JSON给前端）
// ═══════════════════════════════════════════════
if ($action === 'import_grades') {
    if (empty($token)) {
        outputJson(['code' => -1, 'msg' => '请输入用户密钥', 'data' => []]);
    }

    $cfg = getZsosConfig($db);

    if (empty($cfg['uuid']) || empty($cfg['secret_key'])) {
        outputJson(['code' => -1, 'msg' => '管理员尚未配置知岁OS服务商信息，请联系管理员', 'data' => []]);
    }

    $result = fetchZsosData($cfg['uuid'], $token, $cfg['secret_key'], $cfg['base_url']);

    if ($result['code'] !== 0) {
        outputJson(['code' => -1, 'msg' => $result['msg'], 'data' => []]);
    }

    $osData = $result['data'];

    // 解析成绩数据 — 适配知岁OS返回格式
    // 知岁OS格式: { "2023": { "1": [{ course, credit, gpa, achievement }], "2": [...] }, "2024": {...} }
    $courses = [];

    // 提取成绩数据
    $scoreData = $osData;
    if (isset($osData['score']) && is_array($osData['score']) && !isset($osData['score'][0])) {
        $scoreData = $osData['score'];
    } elseif (isset($osData['data']) && is_array($osData['data']) && !isset($osData['data'][0])) {
        $scoreData = $osData['data'];
    }

    // 检测三级嵌套格式：学年 → 学期 → 课程
    $isNested = false;
    foreach ($scoreData as $key => $val) {
        $strKey = (string)$key;
        if (preg_match('/^\d{4}$/', $strKey) && is_array($val)) {
            foreach ($val as $semKey => $semVal) {
                if (is_array($semVal)) {
                    $isNested = true;
                    break 2;
                }
            }
        }
    }

    if ($isNested) {
        foreach ($scoreData as $year => $semesters) {
            if (!is_array($semesters)) continue;
            foreach ($semesters as $term => $courseList) {
                if (!is_array($courseList)) continue;
                foreach ($courseList as $c) {
                    $c['_year'] = (string)$year;
                    $c['_term'] = (string)$term;
                    $courses[] = $c;
                }
            }
        }
    } elseif (isset($scoreData['courses']) && is_array($scoreData['courses'])) {
        $courses = $scoreData['courses'];
    } elseif (isset($scoreData['grades']) && is_array($scoreData['grades'])) {
        $courses = $scoreData['grades'];
    } elseif (isset($scoreData[0]) && is_array($scoreData[0])) {
        $first = $scoreData[0];
        if (isset($first['name']) || isset($first['courseName']) || isset($first['score']) || isset($first['course'])) {
            $courses = $scoreData;
        }
    }

    // 标准化成绩字段
    $normalizedCourses = [];
    foreach ($courses as $c) {
        $year = $c['_year'] ?? $c['year'] ?? $c['schoolYear'] ?? $c['academicYear'] ?? '未知';
        $term = $c['_term'] ?? $c['term'] ?? $c['semester'] ?? '未知';
        if ($term === '1' || $term === 1) $term = '第一学期';
        elseif ($term === '2' || $term === 2) $term = '第二学期';

        $scoreVal = $c['achievement'] ?? $c['score'] ?? $c['totalScore'] ?? '';
        // 兼容文字成绩（优/良/合格等）和数字成绩
        if (is_numeric($scoreVal)) {
            $normalizedScore = (string)floatval($scoreVal);
        } else {
            $normalizedScore = (string)$scoreVal;
        }

        $normalizedCourses[] = [
            'name'    => $c['course'] ?? $c['name'] ?? $c['courseName'] ?? $c['course_name'] ?? '未知课程',
            'score'   => $normalizedScore,
            'credits' => floatval($c['credit'] ?? $c['credits'] ?? $c['creditHours'] ?? 3),
            'gpa'     => floatval($c['gpa'] ?? $c['gradePoint'] ?? 0),
            'year'    => (string)$year,
            'term'    => (string)$term,
            'cat'     => $c['cat'] ?? $c['category'] ?? $c['courseType'] ?? $c['type'] ?? '专业必修',
        ];
    }

    outputJson([
        'code' => 0,
        'msg'  => 'success',
        'data' => ['courses' => $normalizedCourses]
    ]);
}

// ═══════════════════════════════════════════════
// 接口：数据同步入库（前端 zsos-import.php 调用）
// ═══════════════════════════════════════════════
if ($action === 'sync' || ($action === '' && !empty($token) && !empty($type))) {
    // 兼容旧前端调用：无action但有token+type时，视为sync
    if (empty($token)) {
        outputJson(['code' => 0, 'msg' => 'Token不能为空']);
    }

    if (empty($type)) {
        outputJson(['code' => 0, 'msg' => '接口类型不能为空']);
    }

    $allowedTypes = ['userInfo', 'course', 'score', 'progress', 'classroom', 'exam'];
    if (!in_array($type, $allowedTypes)) {
        outputJson(['code' => 0, 'msg' => '不支持的接口类型: ' . $type]);
    }

    $cfg = getZsosConfig($db);

    if (empty($cfg['uuid']) || empty($cfg['secret_key'])) {
        outputJson(['code' => -1, 'msg' => '管理员尚未配置知岁OS服务商信息，请联系管理员']);
    }

    // 调用知岁OS API获取数据
    $result = fetchZsosData($cfg['uuid'], $token, $cfg['secret_key'], $cfg['base_url']);

    if ($result['code'] !== 0) {
        outputJson(['code' => 0, 'msg' => '同步失败: ' . $result['msg']]);
    }

    $data = $result['data'];

    // 根据类型同步数据（userInfo入库，其余存SESSION缓存）
    try {
        switch ($type) {
            case 'userInfo':
                $db->beginTransaction();
                syncUserInfo($db, $userId, $data);
                $db->commit();
                break;
            case 'course':
                syncCourse($db, $userId, $data);
                break;
            case 'score':
                syncScore($db, $userId, $data);
                break;
            case 'progress':
                syncProgress($db, $userId, $data);
                break;
            case 'classroom':
                syncClassroom($db, $userId, $data);
                break;
            case 'exam':
                syncExam($db, $userId, $data);
                break;
        }

        outputJson(['code' => 1, 'msg' => '数据同步成功', 'type' => $type]);
    } catch (Exception $e) {
        if ($type === 'userInfo' && $db->inTransaction()) {
            $db->rollBack();
        }
        outputJson(['code' => 0, 'msg' => '数据同步失败: ' . $e->getMessage()]);
    }
}

// ═══════════════════════════════════════════════
// 接口：测试知岁OS连接（后台用）
// ═══════════════════════════════════════════════
if ($action === 'test_connection') {
    $uuid = trim((string)(($_REQUEST['uuid'] ?? '') ?: ($cfg['uuid'] ?? '')));
    $secretKey = trim((string)(($_REQUEST['secret_key'] ?? '') ?: ($cfg['secret_key'] ?? '')));
    $testToken = trim((string)($_REQUEST['test_token'] ?? ''));

    if (empty($uuid) || empty($secretKey)) {
        outputJson(['code' => -1, 'msg' => '请填写服务商UUID和密钥']);
    }

    if (!empty($testToken)) {
        $baseUrl = defined('ZSOS_BASE_URL') ? ZSOS_BASE_URL : 'https://yun.sx-wl.cn';
        $result = fetchZsosData($uuid, $testToken, $secretKey, $baseUrl);
        outputJson([
            'code' => $result['code'] === 0 ? 0 : -1,
            'msg'  => $result['code'] === 0 ? '连接测试成功，数据解密正常' : $result['msg'],
        ]);
    } else {
        $keyBin = @hex2bin($secretKey);
        if ($keyBin === false || strlen($keyBin) !== 32) {
            outputJson(['code' => -1, 'msg' => '密钥格式错误：需要64位hex字符串（32字节）']);
        } else {
            outputJson(['code' => 0, 'msg' => '密钥格式验证通过（64位hex/32字节）']);
        }
    }
}

// 无效请求
outputJson(['code' => -1, 'msg' => '无效的请求，请指定action参数']);

// ==========================================
//  数据同步入库函数
// ==========================================

/**
 * 同步用户信息 → users 表
 */
function syncUserInfo(PDO $db, int $userId, array $data)
{
    $stmt = $db->prepare("UPDATE users SET real_name = ?, student_id = ? WHERE id = ?");
    $stmt->execute([
        $data['username'] ?? '',
        $data['student_id'] ?? '',
        $userId
    ]);

    $_SESSION['real_name'] = $data['username'] ?? '';
    $_SESSION['student_id'] = $data['student_id'] ?? '';
}

/**
 * 判断数组是否为 progress.courses 嵌套结构（含 compulsory/elective/restricted/other 分类键）
 */
function isProgressCoursesStructure($arr)
{
    if (!is_array($arr) || empty($arr)) return false;
    $categoryKeys = ['compulsory', 'elective', 'restricted', 'other'];
    $matchCount = 0;
    foreach ($categoryKeys as $key) {
        if (isset($arr[$key])) $matchCount++;
    }
    // 有2个及以上分类键 → 认定为 progress.courses 结构
    return $matchCount >= 2;
}

/**
 * 扁平化 progress.courses 嵌套结构，将所有分类的课程合并为一个数组
 */
function flattenProgressCourses($progressCourses)
{
    $flat = [];
    $categories = ['compulsory', 'elective', 'restricted', 'other'];
    foreach ($categories as $cat) {
        if (isset($progressCourses[$cat]) && is_array($progressCourses[$cat])) {
            foreach ($progressCourses[$cat] as $item) {
                if (is_array($item)) {
                    $item['_category'] = $cat;
                    $flat[] = $item;
                }
            }
        }
    }
    return $flat;
}

/**
 * 同步课表 → SESSION缓存（不再入库）
 * 知岁OS格式: { course: [{ name, start, end, floor, weeks, week, teacher }] }
 * 也可能: 直接数组 / { courses: [...] } / { schedule: [...] } / { data: { course: [...] } }
 * 注意: progress.courses 是分类嵌套结构 { compulsory:[], elective:[], restricted:[], other:[] }
 *       需要检测并扁平化，避免只提取到 other 中的一条
 */
function syncCourse(PDO $db, int $userId, array $data)
{
    // 提取课表数组 — 多层兼容，检测 progress.courses 嵌套结构
    $courses = [];
    $scheduleFound = false;

    // 优先提取明确为课表格式的数据（扁平数组，含 start/end/week 等课表字段）
    $candidates = [];

    // 1. 直接检测各键
    if (isset($data['course']) && is_array($data['course']) && !empty($data['course'])) {
        $candidates[] = ['key' => 'course', 'value' => $data['course']];
    }
    if (isset($data['schedule']) && is_array($data['schedule']) && !empty($data['schedule'])) {
        $candidates[] = ['key' => 'schedule', 'value' => $data['schedule']];
    }
    if (isset($data['courses']) && is_array($data['courses']) && !empty($data['courses'])) {
        $candidates[] = ['key' => 'courses', 'value' => $data['courses']];
    }

    // 2. 内层 data 包装
    if (isset($data['data']) && is_array($data['data']) && !empty($data['data'])) {
        if (isset($data['data'][0]) && is_array($data['data'][0])) {
            $candidates[] = ['key' => 'data[]', 'value' => $data['data']];
        } else {
            $innerData = $data['data'];
            if (isset($innerData['course']) && is_array($innerData['course']) && !empty($innerData['course'])) {
                $candidates[] = ['key' => 'data.course', 'value' => $innerData['course']];
            }
            if (isset($innerData['courses']) && is_array($innerData['courses']) && !empty($innerData['courses'])) {
                $candidates[] = ['key' => 'data.courses', 'value' => $innerData['courses']];
            }
            if (isset($innerData['schedule']) && is_array($innerData['schedule']) && !empty($innerData['schedule'])) {
                $candidates[] = ['key' => 'data.schedule', 'value' => $innerData['schedule']];
            }
        }
    }

    // 3. 顶层直接数组
    if (isset($data[0]) && is_array($data[0])) {
        $first = $data[0];
        if (isset($first['name']) || isset($first['courseName']) || isset($first['course_name']) ||
            isset($first['start']) || isset($first['week'])) {
            $candidates[] = ['key' => 'root[]', 'value' => $data];
        }
    }

    // 4. 遍历候选数据，优先选择课表格式（扁平数组含 start/end/week 字段）
    foreach ($candidates as $c) {
        $val = $c['value'];

        // 检测是否为 progress.courses 嵌套结构
        if (isProgressCoursesStructure($val)) {
            // 这是学业分类数据，需要扁平化后提取含课表字段的项
            $flat = flattenProgressCourses($val);
            // 检查扁平化后的项是否含课表字段
            $hasScheduleFields = false;
            foreach ($flat as $item) {
                if (isset($item['start']) || isset($item['end']) || isset($item['week']) || isset($item['weeks'])) {
                    $hasScheduleFields = true;
                    break;
                }
            }
            if ($hasScheduleFields) {
                // progress.courses 中的项包含课表字段 → 使用扁平化后的数据
                $courses = $flat;
                $scheduleFound = true;
                break;
            }
            // progress.courses 中的项不含课表字段 → 跳过，继续找真正的课表数据
            continue;
        }

        // 检测是否为扁平课表数组（第一个元素含课表字段）
        $firstItem = null;
        foreach ($val as $item) {
            if (is_array($item)) {
                $firstItem = $item;
                break;
            }
        }
        if ($firstItem !== null && (
            isset($firstItem['start']) || isset($firstItem['end']) ||
            isset($firstItem['week']) || isset($firstItem['weeks']) ||
            isset($firstItem['startSection']) || isset($firstItem['weekday'])
        )) {
            // 扁平课表数组 → 直接使用
            $courses = $val;
            $scheduleFound = true;
            break;
        }
    }

    // 5. 如果没找到课表格式数据，对 progress.courses 嵌套结构做兜底扁平化
    if (!$scheduleFound) {
        foreach ($candidates as $c) {
            $val = $c['value'];
            if (isProgressCoursesStructure($val)) {
                $courses = flattenProgressCourses($val);
                $scheduleFound = true;
                break;
            }
        }
    }

    // 6. 最后兜底：使用第一个非空的候选
    if (!$scheduleFound && !empty($candidates)) {
        $courses = $candidates[0]['value'];
    }

    if (empty($courses)) {
        throw new Exception('课表数据为空');
    }

    // 获取当前学年学期
    $currentYear = date('Y');
    $currentMonth = date('m');
    $semester = ($currentMonth >= 9 || $currentMonth <= 1) ? '第一学期' : '第二学期';
    $academicYear = ($currentMonth >= 9) ? $currentYear . '-' . ($currentYear + 1) : ($currentYear - 1) . '-' . $currentYear;

    // 为缺少课表字段的 progress.courses 项分配不同位置，避免所有课程挤在同一格
    $posIndex = 0;
    $normalizedCourses = [];
    foreach ($courses as $course) {
        $hasScheduleFields = isset($course['start']) || isset($course['end']) ||
            isset($course['week']) || isset($course['weeks']) ||
            isset($course['startSection']) || isset($course['weekday']) ||
            isset($course['start_section']) || isset($course['end_section']);

        $weeks = $course['weeks'] ?? [1];
        if (is_array($weeks) && !empty($weeks)) {
            $startWeek = min($weeks);
            $endWeek = max($weeks);
        } else {
            $startWeek = intval($course['startWeek'] ?? $course['start_week'] ?? 1);
            $endWeek = intval($course['endWeek'] ?? $course['end_week'] ?? 16);
        }

        if ($hasScheduleFields) {
            // 有课表字段 → 正常提取
            $weekDay = intval($course['week'] ?? $course['weekday'] ?? $course['dayOfWeek'] ?? $course['day'] ?? 1);
            $startSec = intval($course['start'] ?? $course['startSection'] ?? $course['start_section'] ?? $course['sectionStart'] ?? 1);
            $endSec = intval($course['end'] ?? $course['endSection'] ?? $course['end_section'] ?? $course['sectionEnd'] ?? 1);
        } else {
            // 无课表字段（来自 progress.courses 扁平化） → 分散排列避免重叠
            $weekDay = ($posIndex % 7) + 1;          // 周一到周日循环
            $startSec = (($posIndex % 5) * 2) + 1;   // 1,3,5,7,9 循环
            $endSec = $startSec + 1;                  // 每门课占2节
            $posIndex++;
        }

        $normalizedCourses[] = [
            'id' => $posIndex > 0 ? $posIndex : ($course['id'] ?? 0),
            'course_name' => $course['name'] ?? $course['courseName'] ?? $course['course_name'] ?? '',
            'teacher_name' => $course['teacher'] ?? $course['teacherName'] ?? $course['teacher_name'] ?? '',
            'location' => $course['floor'] ?? $course['location'] ?? '',
            'week_day' => $weekDay,
            'start_section' => $startSec,
            'end_section' => $endSec,
            'start_week' => $startWeek,
            'end_week' => $endWeek,
            'semester' => $semester,
            'academic_year' => $academicYear,
        ];
    }

    // 为每条记录分配唯一ID（供前端JS引用）
    foreach ($normalizedCourses as $i => &$c) {
        $c['id'] = $i + 1;
    }
    unset($c);

    // 存入SESSION缓存（不再写入数据库）
    $_SESSION['course_data'] = $normalizedCourses;
    $_SESSION['course_sync_time'] = date('Y-m-d H:i:s');
    $_SESSION['course_semester'] = $semester;
    $_SESSION['course_academic_year'] = $academicYear;
}

/**
 * 同步成绩 → SESSION缓存（不再入库）
 * 知岁OS格式: { "2023": { "1": [{ course, credit, gpa, achievement }], "2": [...] } }
 */
function syncScore(PDO $db, int $userId, array $data)
{
    // 提取成绩数据（可能是包装格式）
    $scoreData = $data;
    if (isset($data['score']) && is_array($data['score'])) {
        $scoreData = $data['score'];
    }

    if (empty($scoreData)) {
        throw new Exception('成绩数据为空');
    }

    // 标准化成绩数据存入SESSION
    $normalizedGrades = [];
    foreach ($scoreData as $academicYear => $semesters) {
        if (!is_array($semesters)) continue;
        foreach ($semesters as $semester => $courses) {
            if (!is_array($courses)) continue;
            foreach ($courses as $course) {
                // score 兼容文字成绩（优/良/合格等）和数字成绩
                $scoreVal = $course['achievement'] ?? $course['score'] ?? '';
                if (is_numeric($scoreVal)) {
                    $scoreStr = (string)floatval($scoreVal);
                } else {
                    $scoreStr = (string)$scoreVal;
                }
                $normalizedGrades[] = [
                    'course_name' => $course['course'] ?? $course['name'] ?? '',
                    'credit' => floatval($course['credit'] ?? 0),
                    'score' => $scoreStr,
                    'grade_point' => floatval($course['gpa'] ?? 0),
                    'semester' => '第' . $semester . '学期',
                    'academic_year' => $academicYear,
                ];
            }
        }
    }

    // 存入SESSION缓存（不再写入数据库）
    $_SESSION['score_data'] = $normalizedGrades;
    $_SESSION['score_sync_time'] = date('Y-m-d H:i:s');
}

/**
 * 同步学业进度 → SESSION缓存（不再入库）
 * 知岁OS格式: { gpa, all, pass, fail, unfinished, studying, time, courses: { compulsory, elective, restricted, other } }
 * 也可能: { progress: { ... } } / { data: { ... } } 包装格式
 */
function syncProgress(PDO $db, int $userId, array $data)
{
    // 提取学业进度数据（可能被包装了一层）
    $progressData = $data;
    if (isset($data['progress']) && is_array($data['progress'])) {
        $progressData = $data['progress'];
    } elseif (isset($data['data']) && is_array($data['data']) && !isset($data['data'][0])) {
        $progressData = $data['data'];
    }

    // 兼容：courses 数据可能在 'course' 键中（知岁OS返回的 course 字段为 progress.courses 嵌套结构）
    if (!isset($progressData['courses']) && isset($progressData['course'])) {
        if (is_array($progressData['course']) && (isset($progressData['course']['compulsory']) || isset($progressData['course']['elective']) || isset($progressData['course']['restricted']) || isset($progressData['course']['other']))) {
            $progressData['courses'] = $progressData['course'];
        }
    }

    // 兼容：顶层 courses 嵌套对象（知岁OS返回 courses: {compulsory:[], ...}）
    if (!isset($progressData['courses']) && isset($data['courses']) && is_array($data['courses'])) {
        if (isset($data['courses']['compulsory']) || isset($data['courses']['elective']) || isset($data['courses']['restricted']) || isset($data['courses']['other'])) {
            $progressData['courses'] = $data['courses'];
        }
    }

    $gpa = floatval($progressData['gpa'] ?? $progressData['gradePoint'] ?? 0);
    $allCount = intval($progressData['all'] ?? $progressData['total'] ?? $progressData['totalCourses'] ?? 0);

    if ($gpa == 0 && $allCount == 0) {
        throw new Exception('学业数据为空或格式不匹配');
    }

    // 存入SESSION缓存（不再写入数据库）
    $_SESSION['progress_data'] = $progressData;
    $_SESSION['progress_update_time'] = $progressData['time'] ?? date('Y-m-d H:i:s');
    $_SESSION['progress_sync_time'] = date('Y-m-d H:i:s');
}

/**
 * 同步空教室 → SESSION缓存（不再入库）
 */
function syncClassroom(PDO $db, int $userId, array $data)
{
    if (empty($data)) {
        throw new Exception('空教室数据为空');
    }

    // 存入SESSION缓存（不再写入数据库）
    $_SESSION['classroom_data'] = $data;
    $_SESSION['classroom_sync_time'] = date('Y-m-d H:i:s');
}

/**
 * 同步考试安排 → session缓存（规范化字段名）
 */
function syncExam(PDO $db, int $userId, array $data)
{
    if (empty($data)) {
        throw new Exception('考试安排数据为空');
    }

    // 考试数据可能是嵌套结构，先提取考试数组
    $examList = [];
    if (isset($data['exam']) && is_array($data['exam'])) {
        $examList = $data['exam'];
    } elseif (isset($data['exams']) && is_array($data['exams'])) {
        $examList = $data['exams'];
    } elseif (isset($data['data']) && is_array($data['data'])) {
        $examList = $data['data'];
    } elseif (isset($data[0]) && is_array($data[0])) {
        $examList = $data;
    } else {
        // 单条考试数据
        $examList = [$data];
    }

    // 规范化字段名 → name/date/time/location/seat/type
    $normalized = [];
    foreach ($examList as $e) {
        if (!is_array($e)) continue;
        $normalized[] = [
            'name'     => $e['name'] ?? $e['course'] ?? $e['examName'] ?? $e['course_name'] ?? $e['subject'] ?? '未知考试',
            'date'     => $e['date'] ?? $e['examDate'] ?? $e['exam_date'] ?? $e['startDate'] ?? '',
            'time'     => $e['time'] ?? $e['examTime'] ?? $e['exam_time'] ?? $e['startTime'] ?? '',
            'location' => $e['location'] ?? $e['place'] ?? $e['examLocation'] ?? $e['exam_location'] ?? $e['room'] ?? $e['floor'] ?? '',
            'seat'     => $e['seat'] ?? $e['seatNumber'] ?? $e['seat_number'] ?? $e['seatNo'] ?? '',
            'type'     => $e['type'] ?? $e['examType'] ?? $e['exam_type'] ?? $e['category'] ?? '',
        ];
    }

    $_SESSION['exam_data'] = $normalized;
    $_SESSION['exam_update_time'] = date('Y-m-d H:i:s');
    $_SESSION['exam_sync_time'] = date('Y-m-d H:i:s');
}
