<?php
namespace app\adminVue\service;

use think\facade\Db;

class ExamQuestionBank
{
    public static function question(array $row, $line = null)
    {
        $prefix = $line === null ? '' : '第' . $line . '行：';
        $type = (string)($row['exam_type'] ?? '');
        if (!in_array($type, ['1', '2', '3'], true)) throw new \InvalidArgumentException($prefix . '题目类型必须为1、2或3');
        $data = ['exam_type'=>(int)$type];
        foreach (['content', 'item_a', 'item_b', 'item_c', 'item_d'] as $i => $field) {
            if (isset($row[$field]) && !is_scalar($row[$field])) throw new \InvalidArgumentException($prefix . '题目和选项必须为文本');
            $value = trim((string)($row[$field] ?? ''));
            if (($i < 3 && $value === '') || mb_strlen($value, 'UTF-8') > 255) throw new \InvalidArgumentException($prefix . ($i === 0 ? '题目' : '选项' . $i) . '不能为空且不能超过255字（选项3、4可留空）');
            if ($i > 0 && $value !== '' && (!preg_match('/^' . $i . '\.[\s\S]*\S$/u', $value) || trim(substr($value, 2)) === '')) throw new \InvalidArgumentException($prefix . '选项' . $i . '必须以' . $i . '.开头且包含选项内容');
            $data[$field] = $value;
        }
        if ($data['item_d'] !== '' && $data['item_c'] === '') throw new \InvalidArgumentException($prefix . '选项3为空时不能填写选项4');
        if ($type === '3' && ($data['item_c'] !== '' || $data['item_d'] !== '')) throw new \InvalidArgumentException($prefix . '判断题只能填写选项1、2');
        $answer = $row['answer'] ?? '';
        if (!is_string($answer) || !preg_match('/^\s*\[/', $answer)) throw new \InvalidArgumentException($prefix . '答案必须为JSON数组，例如[1]或[1,3]');
        $answer = json_decode($answer, true);
        if (json_last_error() !== JSON_ERROR_NONE || !is_array($answer) || !$answer || array_keys($answer) !== range(0, count($answer) - 1)) throw new \InvalidArgumentException($prefix . '答案必须为非空JSON数组');
        $keys = [];
        foreach ($answer as $key) {
            if ((!is_int($key) && !is_string($key)) || !in_array((string)$key, ['1','2','3','4'], true) || $data[['1'=>'item_a','2'=>'item_b','3'=>'item_c','4'=>'item_d'][(string)$key]] === '') throw new \InvalidArgumentException($prefix . '答案必须对应已填写的选项编号1至4');
            $keys[] = (int)$key;
        }
        if (count(array_unique($keys)) !== count($keys) || ($type !== '2' && count($keys) !== 1) || ($type === '2' && count($keys) < 2)) throw new \InvalidArgumentException($prefix . '答案不能重复，单选/判断须选1项，多选须至少选2项');
        sort($keys);
        $data['answer'] = json_encode($keys);
        return $data;
    }

    public static function rows($rows, array $skills)
    {
        if (!is_array($rows) || !$rows || count($rows) > 5000) throw new \InvalidArgumentException('请上传1至5000道试题');
        $names = [];
        foreach ($skills as $skill) $names[trim((string)$skill['content'])][] = (int)$skill['id'];
        $result = [];
        foreach (array_values($rows) as $i => $row) {
            if (!is_array($row)) throw new \InvalidArgumentException('第' . ($i + 2) . '行格式错误');
            $line = isset($row['_row']) && is_int($row['_row']) && $row['_row'] > 1 ? $row['_row'] : $i + 2;
            $name = $row['skill_name'] ?? '';
            if (!is_string($name) || trim($name) === '') throw new \InvalidArgumentException('第' . $line . '行：技能名称不能为空');
            $name = trim($name);
            if (empty($names[$name])) throw new \InvalidArgumentException('第' . $line . '行：未找到技能名称“' . $name . '”，整批上传已停止');
            if (count($names[$name]) !== 1) throw new \InvalidArgumentException('第' . $line . '行：技能名称“' . $name . '”存在重名，请先核对技能配置');
            $result[] = self::question($row, $line) + ['item_type'=>$names[$name][0]];
        }
        return $result;
    }

    public static function version(array $row)
    {
        // Normalize PDO/ORM integer types before hashing the same database record.
        ksort($row);
        return hash('sha256', json_encode(array_map(function ($value) { return $value === null ? null : (string)$value; }, $row), JSON_UNESCAPED_UNICODE));
    }

    private static function snapshot(array $rows, array $skills, array $old)
    {
        $groups = [];
        $names = array_column($skills, 'content', 'id');
        foreach ($rows as $row) {
            $id = $row['item_type'];
            if (!isset($groups[$id])) $groups[$id] = ['item_type'=>$id, 'name'=>$names[$id], 'new_count'=>0, 'old_count'=>0];
            $groups[$id]['new_count']++;
        }
        $versions = [];
        foreach ($old as $row) {
            $groups[$row['item_type']]['old_count']++;
            $versions[] = self::version($row);
        }
        sort($versions);
        return ['groups'=>array_values($groups), 'total'=>count($rows), 'stamp'=>hash('sha256', json_encode([$rows, $groups, $versions], JSON_UNESCAPED_UNICODE))];
    }

    public static function preview($input)
    {
        $skills = Db::name('quality_base')->where(['mid'=>4, 'status'=>1])->order('id')->select()->toArray();
        $rows = self::rows($input, $skills);
        $old = Db::name('quality_ppi_exam')->whereIn('item_type', array_unique(array_column($rows, 'item_type')))->where('status', 1)->select()->toArray();
        return self::snapshot($rows, $skills, $old);
    }

    private static function locked($action)
    {
        Db::query('SELECT 1');
        $connection = Db::connect()->getConnection();
        $prefix = $connection->getConfig('prefix');
        if (!preg_match('/^[a-zA-Z0-9_]+$/', $prefix)) throw new \RuntimeException('数据库表前缀配置错误');
        $pdo = $connection->getPdo();
        $exam = '`' . $prefix . 'quality_ppi_exam`';
        $base = '`' . $prefix . 'quality_base`';
        $pdo->exec('LOCK TABLES ' . $exam . ' WRITE, ' . $base . ' READ');
        try { return $action($pdo, $exam, $base); }
        finally { $pdo->exec('UNLOCK TABLES'); }
    }

    public static function import($input, $stamp, $overwrite)
    {
        return self::locked(function ($pdo, $exam, $base) use ($input, $stamp, $overwrite) {
            $skills = $pdo->query('SELECT * FROM ' . $base . ' WHERE mid=4 AND status=1 ORDER BY id')->fetchAll(\PDO::FETCH_ASSOC);
            $rows = self::rows($input, $skills);
            $ids = implode(',', array_unique(array_column($rows, 'item_type')));
            $old = $pdo->query('SELECT * FROM ' . $exam . ' WHERE status=1 AND item_type IN (' . $ids . ')')->fetchAll(\PDO::FETCH_ASSOC);
            $snapshot = self::snapshot($rows, $skills, $old);
            if (!is_string($stamp) || !hash_equals($snapshot['stamp'], $stamp)) throw new \InvalidArgumentException('试题或技能配置已变化，请重新检查文件并确认覆盖');
            if ($old && $overwrite !== true) throw new \InvalidArgumentException('已有试卷必须确认覆盖后才能上传');
            $created = [];
            $oldIds = array_column($old, 'id');
            // ponytail: MyISAM uses a table lock and compensating writes; process/database crashes require InnoDB for full rollback guarantees.
            try {
                $insert = $pdo->prepare('INSERT INTO ' . $exam . ' (exam_type,content,item_a,item_b,item_c,item_d,answer,item_type,status) VALUES (?,?,?,?,?,?,?,?,0)');
                foreach ($rows as $row) {
                    $insert->execute([$row['exam_type'],$row['content'],$row['item_a'],$row['item_b'],$row['item_c'],$row['item_d'],$row['answer'],$row['item_type']]);
                    $created[] = (int)$pdo->lastInsertId();
                }
                $pdo->exec('UPDATE ' . $exam . ' SET status=CASE WHEN id IN (' . implode(',', $created) . ') THEN 1 ELSE 0 END WHERE item_type IN (' . $ids . ')');
            } catch (\Throwable $error) {
                try {
                    if ($created) $pdo->exec('DELETE FROM ' . $exam . ' WHERE id IN (' . implode(',', $created) . ')');
                    if ($oldIds) $pdo->exec('UPDATE ' . $exam . ' SET status=1 WHERE id IN (' . implode(',', array_map('intval', $oldIds)) . ')');
                } catch (\Throwable $restoreError) {
                    throw new \RuntimeException('导入失败且旧题恢复失败，请联系管理员检查题库', 0, $restoreError);
                }
                throw new \RuntimeException('导入失败，已清理新题并恢复旧题，请重试', 0, $error);
            }
            return $snapshot;
        });
    }

    public static function change($id, $version, $input = null)
    {
        if (filter_var($id, FILTER_VALIDATE_INT, ['options'=>['min_range'=>1]]) === false) throw new \InvalidArgumentException('题目ID无效');
        $data = $input === null ? null : self::question($input);
        return self::locked(function ($pdo, $exam) use ($id, $version, $data) {
            $query = $pdo->prepare('SELECT * FROM ' . $exam . ' WHERE id=? AND status=1');
            $query->execute([$id]);
            $old = $query->fetch(\PDO::FETCH_ASSOC);
            if (!$old) throw new \InvalidArgumentException('题目已停用或不存在，请刷新列表');
            if (!is_string($version) || !hash_equals(self::version($old), $version)) throw new \InvalidArgumentException('题目已被修改，请刷新后重新操作');
            if ($data === null) {
                $query = $pdo->prepare('UPDATE ' . $exam . ' SET status=0 WHERE id=?');
                $query->execute([$id]);
            } else {
                $query = $pdo->prepare('UPDATE ' . $exam . ' SET exam_type=?,content=?,item_a=?,item_b=?,item_c=?,item_d=?,answer=? WHERE id=?');
                $query->execute([$data['exam_type'],$data['content'],$data['item_a'],$data['item_b'],$data['item_c'],$data['item_d'],$data['answer'],$id]);
            }
        });
    }
}
