<?php
namespace app\mobile\controller;

use think\facade\Db;

/** 关键岗位考试：本人安排、服务器试卷及计分，成绩只在提高时更新。 */
class OmsKeyPositionExam extends OmsBase
{
    public function messageItems($account,$since,\app\mobile\service\MessageInbox $inbox) {
        if(!$inbox->allowsSource($this))$this->fail('接口不存在',404);
        $member=self::memberFor($account);if(!$member)return [];$items=[];$now=$this->now();
        $rows=Db::name('quality_newskill_score')->where('mid',$member['id'])->where('exam_date','>',$now-3600)->whereIn('status',[1,3])->select()->toArray();
        $names=$rows?Db::name('quality_base')->whereIn('id',array_column($rows,'item_type'))->column('content','id'):[];
        foreach($rows as $row){$base=Db::name('quality_base')->where(['id'=>$row['item_type'],'status'=>1])->find();if(!$base || !ctype_digit((string)$base['mark']) || (int)$base['mark']<=0)continue;
            $started=$now>=(int)$row['exam_date'];$items[]=\app\mobile\service\MessageInbox::item('key-exam',$row['id'],'exam','关键岗位考试'.($started?'待参加':'安排'),$started?((int)$row['status']===3?'可重考':'待考试'):'未开始',['考试项目'=>$names[$row['item_type']] ?? '','开始时间'=>$this->formatDate($row['exam_date'])],$started?'/pages/key-position-exam/paper?id='.$row['id']:'/pages/key-position-exam/index',$started?'pending':'notice',$row['exam_date'],$row['exam_date']+3600);
        }
        foreach(Db::name('quality_newskill_score')->where(['mid'=>$member['id'],'status'=>2])->where('exam_date','>=',$since)->select()->toArray() as $row)$items[]=\app\mobile\service\MessageInbox::item('key-exam',$row['id'],'exam','关键岗位考试已通过','已通过',['考试项目'=>Db::name('quality_base')->where('id',$row['item_type'])->value('content') ?: '','成绩'=>$row['score']],'/pages/key-position-exam/index','done',$row['exam_date']);
        return $items;
    }
    protected function now() { return time(); }
    public static function memberFor($account) { return OmsLeaveApply::memberFor($account); }
    private function member() { $member=self::memberFor($this->current());if(!$member)$this->fail('仅关联在职现场人员的本人可参加考试',403);return $member; }
    private function result($data) { return json(['code'=>0,'message'=>'成功','data'=>$data]); }
    private function id($value) { if(!is_scalar($value) || !preg_match('/^[1-9][0-9]{0,9}$/',(string)$value) || (float)$value>2147483647)$this->fail('考试编号不正确');return (int)$value; }
    private function formatDate($time) { return (new \DateTimeImmutable('@'.(int)$time))->setTimezone(new \DateTimeZone('Asia/Shanghai'))->format('Y-m-d H:i:s'); }
    private function configError() { $this->fail('该项目考试配置异常，请向相关负责部门反馈，本次考试已终止',409,2101); }
    private function cachePut($key,$value,$ttl) { if(!$this->cache()->set($key,$value,max(1,$ttl)))$this->fail('考试缓存不可用，请稍后重试',503); }
    private function paperKey($mid,$id) { return $this->cacheKey('key-exam:paper:'.$mid.':'.$id); }
    private function receiptKey($mid,$paperId) { return $this->cacheKey('key-exam:result:'.$mid.':'.$paperId); }
    private function receipt($mid,$paperId,$row) {
        $key=$this->receiptKey($mid,$paperId);$receipt=$this->cache()->get($key);
        if($receipt && (($row['answers']===null || $row['answers']==='') || (int)round((float)$row['score']*10)<(int)round($receipt['result']['highestScore']*10) || ($receipt['result']['passed'] && (int)$row['status']!==2))){$this->cache()->delete($key);return null;}
        return $receipt;
    }
    private function serial($mid,$id,$action) {
        $key=$this->cacheKey('key-exam:lock:'.$mid.':'.$id);$lock='key-exam:'.sha1($key);$acquired=Db::query('SELECT GET_LOCK(?,5) acquired',[$lock]);if((int)$acquired[0]['acquired']!==1)$this->fail('考试正在处理，请稍后重试',503);
        try{return \app\mobile\service\KeyTraining::locked(function()use($mid,$action){try{\app\mobile\service\KeyTraining::assertExamMember($mid);}catch(\RuntimeException $e){$this->fail($e->getMessage(),409);}return $action();});}finally{Db::query('SELECT RELEASE_LOCK(?)',[$lock]);}
    }
    // ponytail: 低频技能考试共用表锁，安排与交卷使用同一锁；MyISAM崩溃不能事务回滚。
    public static function tableLock() {
        Db::query('SELECT 1');$connection=Db::connect()->getConnection();$prefix=$connection->getConfig('prefix');if(!preg_match('/^[a-zA-Z0-9_]+$/',$prefix))throw new \RuntimeException('Invalid prefix');$pdo=$connection->getPdo();
        $pdo->exec('LOCK TABLES `'.$prefix.'quality_newskill_score` WRITE, `'.$prefix.'member_skill` WRITE, `'.$prefix.'member_list` READ, `'.$prefix.'quality_base` READ');return $pdo;
    }
    /** 后台安排入口复用，全部校验通过后再写入；零题配置在开考时向考生警告。 */
    public static function schedule($eids,$item,$date,$trainingJobId=0) {
        return \app\mobile\service\KeyTraining::locked(function()use($eids,$item,$date,$trainingJobId){
            if(!is_array($eids)||!$eids)throw new \RuntimeException('请填写有效人员');
            $service=new \app\mobile\service\KeyTraining();
            $service->paper($item);
            foreach($eids as $eid){$service->employee((string)$eid);$service->eligible((string)$eid);}
            \app\mobile\service\KeyTraining::assertExamJob($trainingJobId,$eids,$item,$date);
            return self::scheduleLocked($eids,$item,$date,$trainingJobId);
        });
    }
    private static function scheduleLocked($eids,$item,$date,$trainingJobId) {
        if(!is_array($eids) || !$eids || count($eids)>1000 || $date<=0 || $date>2147480047)throw new \RuntimeException('请填写有效人员、项目和考试时间');
        $eids=array_values(array_unique($eids));$lock=self::tableLock();$created=[];
        try {
            $base=Db::name('quality_base')->where(['id'=>$item,'status'=>1])->find();if(!$base || !ctype_digit((string)$base['mark']) || (int)$base['mark']<=0)throw new \RuntimeException('考试项目技能映射不正确，请联系负责部门');
            $members=Db::name('member_list')->where('status',1)->whereIn('eid',$eids)->select()->toArray();$found=array_column($members,'eid');$missing=array_diff($eids,$found);if($missing)throw new \RuntimeException('以下工号不存在或已停用：'.implode('、',$missing));
            if($trainingJobId && Db::name('quality_newskill_score')->where('training_job_id',$trainingJobId)->count())return 1;
            foreach($members as $member)if(Db::name('quality_newskill_score')->where(['mid'=>$member['id'],'item_type'=>$item,'exam_date'=>$date])->count())throw new \RuntimeException('存在重复考试安排：'.$member['eid']);
            try {foreach($members as $member)$created[]=Db::name('quality_newskill_score')->insertGetId(['mid'=>$member['id'],'item_type'=>$item,'exam_date'=>$date,'score'=>null,'answers'=>null,'status'=>1,'tid'=>(int)$member['tid'],'aid'=>0,'poid'=>(int)$member['poid'],'training_job_id'=>$trainingJobId?:null]);}
            catch(\Throwable $error){if($created)Db::name('quality_newskill_score')->whereIn('id',$created)->delete();throw $error;}
        }finally{$lock->exec('UNLOCK TABLES');}
        static::notifyArrangements($members,$base,$date);
        return count($created);
    }
    protected static function notifyArrangements($members,$base,$date) {
        try {
            $service=app()->make(\app\mobile\service\WechatTemplateService::class);
            foreach ($members as $member) {
                try {
                    $service->send('exam_arranged',$service->employees([$member['eid']]),[
                        'name'=>$base['content'],'employee'=>$member['ename'],'deadline'=>$service::time($date+3600),
                    ],'pages/key-position-exam/index');
                } catch (\Throwable $error) { \think\facade\Log::warning('key exam recipient notification unavailable: '.$error->getMessage()); }
            }
        } catch (\Throwable $error) { \think\facade\Log::warning('key exam notification unavailable: '.$error->getMessage()); }
    }
    private function arrangement($mid,$id) { $row=Db::name('quality_newskill_score')->where(['id'=>$id,'mid'=>$mid])->find();if(!$row)$this->fail('考试不存在或不属于本人',403);return $row; }
    private function assertWindow($row) {
        if((int)$row['status']===2)$this->fail('该考试已通过，不能重新考试',409);
        if(!in_array((int)$row['status'],[1,3],true))$this->fail('该考试不可参加',409);
        if($this->now()<(int)$row['exam_date'])$this->fail('考试尚未开始',409);if($this->now()>=(int)$row['exam_date']+3600)$this->fail('考试时间已结束',409,2102);
    }
    private function skill($row) {
        $base=Db::name('quality_base')->where(['id'=>$row['item_type'],'status'=>1])->find();if(!$base || !ctype_digit((string)$base['mark']) || (int)$base['mark']<=0)$this->configError();return $base;
    }
    private function settings($item) {
        $rows=Db::name('quality_exam_set')->where('item_type',$item)->select()->toArray();if(count($rows)!==1)$this->configError();$set=$rows[0];$total=0;$questions=0;
        foreach(range(1,3) as $i){$num=(int)$set['num'.$i];$points=(float)$set['score'.$i];if($num<0 || !is_finite($points) || $points<0 || ($num>0 && $points<=0))$this->configError();$questions+=$num;$total+=$num*(int)round($points*10);}
        if(!$questions || $questions>60 || (float)$set['pass']<=0 || (int)round((float)$set['pass']*10)>$total || $total>99999999)$this->configError();return [$set,$total];
    }
    public function records() { return $this->guarded(function(){
        $this->requireMethod('GET');$member=$this->member();(new \app\mobile\service\KeyTraining())->reconcile(Db::name('kt_registration')->where('username',(string)$member['eid'])->column('id'));$page=max(1,(int)$this->request->get('page',1));$scope=$this->request->get('scope','all');if(!in_array($scope,['all','active'],true))$this->fail('查询范围不正确');
        $query=Db::name('quality_newskill_score')->where('mid',$member['id']);if($scope==='active')$query->whereIn('status',[1,3])->where('exam_date','>',$this->now()-3600);$total=(clone $query)->count();$rows=$query->order('exam_date desc,id desc')->limit(($page-1)*15,15)->select()->toArray();$items=[];
        $names=Db::name('quality_base')->whereIn('id',array_column($rows,'item_type'))->column('content','id');$sets=Db::name('quality_exam_set')->whereIn('item_type',array_column($rows,'item_type'))->select()->toArray();$settings=[];foreach($sets as $set)$settings[$set['item_type']]=$set;
        foreach($rows as $row){$set=$settings[$row['item_type']] ?? null;$full=0;if($set)foreach(range(1,3) as $i)$full+=(int)$set['num'.$i]*(float)$set['score'.$i];$state=(int)$row['status']===2?'已通过':($this->now()<$row['exam_date']?'未开始':($this->now()>=$row['exam_date']+3600?'已结束':((int)$row['status']===3?'可重考':'可考试')));$items[]=['id'=>(int)$row['id'],'name'=>$names[$row['item_type']] ?? '项目已移除','start'=>$this->formatDate($row['exam_date']),'end'=>$this->formatDate($row['exam_date']+3600),'expiresAt'=>((int)$row['exam_date']+3600)*1000,'score'=>$row['answers']!==null && $row['answers']!==''?(float)$row['score']:null,'passScore'=>$set?(float)$set['pass']:null,'fullScore'=>$set?$full:null,'state'=>$state,'canStart'=>in_array($state,['可重考','可考试'],true)];}
        return $this->result(['items'=>$items,'total'=>$total,'page'=>$page,'serverTime'=>$this->now()*1000]);
    }); }
    public function start() { return $this->guarded(function(){
        $this->requireMethod('POST');$member=$this->member();$id=$this->id($this->request->post('id'));$restart=$this->request->post('restart',false);if(!is_bool($restart))$this->fail('重考参数不正确');
        return $this->serial($member['id'],$id,function()use($member,$id,$restart){
            $row=$this->arrangement($member['id'],$id);$this->assertWindow($row);$base=$this->skill($row);
            $key=$this->paperKey($member['id'],$id);$paper=$this->cache()->get($key);
            if($paper && ((int)$paper['examDate']!==(int)$row['exam_date'] || (int)$paper['itemType']!==(int)$row['item_type']))$paper=null;
            if($paper){$receipt=$this->receipt($member['id'],$paper['paperId'],$row);if($restart && !$receipt)$this->fail('请先完成当前试卷，不能重新抽题',409);if(!$restart)return $this->result($this->publicPaper($paper,$receipt['result'] ?? null));}
            [$set,$full]=$this->settings($row['item_type']);$questions=[];
            foreach(range(1,3) as $type){$count=(int)$set['num'.$type];if(!$count)continue;$pool=Db::name('quality_ppi_exam')->where(['item_type'=>$row['item_type'],'exam_type'=>$type,'status'=>1])->select()->toArray();if(count($pool)<$count)$this->configError();shuffle($pool);
                foreach(array_slice($pool,0,$count) as $question){$options=[];foreach(['item_a','item_b','item_c','item_d'] as $field)if(isset($question[$field]) && $question[$field]!==''){$value=(string)$question[$field];if(!preg_match('/^[A-Za-z0-9][.、:：\s]/u',$value))$this->configError();$options[]=['key'=>substr($value,0,1),'text'=>mb_substr($value,2,null,'UTF-8')];}
                    $answer=json_decode($question['answer'],true);if(!is_array($answer) || !$answer || count($options)<2)$this->configError();$answer=array_map('strval',$answer);sort($answer,SORT_STRING);$keys=array_column($options,'key');if(count(array_unique($keys))!==count($keys) || count(array_unique($answer))!==count($answer) || array_diff($answer,$keys) || ($type!==2 && count($answer)!==1))$this->configError();if($type!==3)shuffle($options);
                    $questions[]=['id'=>(int)$question['id'],'type'=>$type,'content'=>$question['content'],'options'=>$options,'points'=>(int)round((float)$set['score'.$type]*10),'correct'=>$answer];
                }
            }
            $paper=['paperId'=>bin2hex(random_bytes(16)),'examId'=>$id,'mid'=>(int)$member['id'],'examDate'=>(int)$row['exam_date'],'itemType'=>(int)$row['item_type'],'skillId'=>(int)$base['mark'],'name'=>$base['content'],'expiresAt'=>(int)$row['exam_date']+3600,'fullScore'=>$full,'passScore'=>(int)round((float)$set['pass']*10),'questions'=>$questions];$this->cachePut($key,$paper,$paper['expiresAt']-$this->now());return $this->result($this->publicPaper($paper));
        });
    }); }
    private function publicPaper($paper,$result=null) {
        $questions=[];foreach($paper['questions'] as $question){unset($question['correct']);$question['points']/=10;$questions[]=$question;}
        return ['examId'=>$paper['examId'],'paperId'=>$paper['paperId'],'name'=>$paper['name'],'expiresAt'=>$paper['expiresAt']*1000,'serverTime'=>$this->now()*1000,'fullScore'=>$paper['fullScore']/10,'passScore'=>$paper['passScore']/10,'questions'=>$questions,'result'=>$result];
    }
    private function grade($paper,$answers) {
        if(!is_array($answers) || count($answers)!==count($paper['questions']))$this->fail('请完成全部题目');$provided=[];
        foreach($answers as $answer){if(!is_array($answer) || !isset($answer['id'],$answer['selected']) || !is_int($answer['id']) || !is_array($answer['selected']) || !$answer['selected'] || count($answer['selected'])>4 || isset($provided[$answer['id']]))$this->fail('答案格式不正确');foreach($answer['selected'] as $key)if(!is_string($key) || !preg_match('/^[A-Za-z0-9]$/',$key))$this->fail('答案格式不正确');if(count(array_unique($answer['selected']))!==count($answer['selected']))$this->fail('答案不能重复');$selected=$answer['selected'];sort($selected,SORT_STRING);$provided[$answer['id']]=$selected;}
        $points=0;$review=[];$stored=[];foreach($paper['questions'] as $question){$selected=$provided[$question['id']] ?? null;if(!$selected || array_diff($selected,array_column($question['options'],'key')) || ($question['type']!==2 && count($selected)!==1))$this->fail('题目或选项不属于本次试卷');$correct=$selected===$question['correct'];if($correct)$points+=$question['points'];$stored[$question['id']]='['.implode(',',$selected).']';$review[]=['id'=>$question['id'],'selected'=>$selected,'correct'=>$question['correct'],'isCorrect'=>$correct];}
        $encoded=json_encode($stored,JSON_UNESCAPED_UNICODE);if(strlen($encoded)>1000)$this->configError();return [$points,$encoded,$review];
    }
    protected function insertSkill($data) { return Db::name('member_skill')->insertGetId($data); }
    public function submit() { return $this->guarded(function(){
        $this->requireMethod('POST');$member=$this->member();$id=$this->id($this->request->post('id'));$paperId=$this->request->post('paperId');if(!is_string($paperId) || !preg_match('/^[a-f0-9]{32}$/',$paperId))$this->fail('试卷编号不正确');$answers=$this->request->post('answers');
        return $this->serial($member['id'],$id,function()use($member,$id,$paperId,$answers){
            $currentRow=$this->arrangement($member['id'],$id);$receiptKey=$this->receiptKey($member['id'],$paperId);$receipt=$this->receipt($member['id'],$paperId,$currentRow);$fingerprint=hash('sha256',json_encode($answers));if($receipt){if($receipt['examId']!==$id || $receipt['fingerprint']!==$fingerprint)$this->fail('该试卷已提交，不能改变答案',409);return $this->result($receipt['result']);}
            $paper=$this->cache()->get($this->paperKey($member['id'],$id));if(!$paper || $paper['paperId']!==$paperId)$this->fail('试卷已失效，请返回考试列表重新进入',409);[$points,$encoded,$review]=$this->grade($paper,$answers);
            $lock=self::tableLock();$inserted=null;$before=null;
            try {
                $fresh=self::memberFor(['user_id'=>$member['id'],'username'=>$member['eid']]);if(!$fresh)$this->fail('员工身份已停用',403);$row=$this->arrangement($member['id'],$id);$this->assertWindow($row);if((int)$row['exam_date']!==$paper['examDate'] || (int)$row['item_type']!==$paper['itemType'] || $this->now()>=$paper['expiresAt'])$this->fail('考试安排已变更或已结束',409);
                $base=$this->skill($row);if((int)$base['mark']!==$paper['skillId'])$this->fail('技能映射已变更，请联系负责部门',409);$before=$row;$highest=(int)round((float)$row['score']*10);$first=$row['answers']===null || $row['answers']==='';$improved=$first || $points>$highest;$passed=$points>=$paper['passScore'];$skillExists=false;
                if($passed){$skillExists=Db::name('member_skill')->where(['mid'=>$member['id'],'skill_id'=>$paper['skillId'],'status'=>1])->count()>0;if(!$skillExists){$today=(new \DateTimeImmutable('@'.$this->now()))->setTimezone(new \DateTimeZone('Asia/Shanghai'))->setTime(0,0);$inserted=$this->insertSkill(['mid'=>$member['id'],'skill_id'=>$paper['skillId'],'eff_date'=>$today->modify('+1 month')->getTimestamp(),'level'=>1,'skill_type'=>2,'audit_type'=>21,'status'=>1,'update'=>$this->now(),'mark'=>'关键岗位考试通过']);}}
                if($improved)Db::name('quality_newskill_score')->where('id',$id)->update(['score'=>$points/10,'answers'=>$encoded,'status'=>$passed?2:3,'tid'=>(int)$fresh['tid'],'poid'=>(int)$fresh['poid']]);
                $result=['examId'=>$id,'paperId'=>$paperId,'score'=>$points/10,'highestScore'=>($improved?$points:$highest)/10,'passed'=>$passed,'updated'=>$improved,'skillExists'=>$skillExists,'review'=>$review];$this->cachePut($receiptKey,['examId'=>$id,'fingerprint'=>$fingerprint,'result'=>$result],86400);return $this->result($result);
            }catch(\Throwable $error){if($before)Db::name('quality_newskill_score')->where('id',$id)->update($before);if($inserted)Db::name('member_skill')->where('id',$inserted)->delete();throw $error;}finally{$lock->exec('UNLOCK TABLES');}
        });
    }); }
}
