<?php
namespace app\mobile\controller;
use think\facade\Db;

/** 与管理端共用原始地址、事件和记录；不创建专用数据库连接。 */
class OmsInventory extends OmsBase
{
    private function run($method,$work){return $this->guarded(function()use($method,$work){$this->requireMethod($method);$a=$this->current();if(!\app\mobile\service\MobileFeatureAccess::allows('eInventory',\app\mobile\service\MobileFeatureAccess::context($a)))$this->fail('电子盘点仅生产部可使用',403);$m=Db::name('member_list')->where(['id'=>(int)$a['user_id'],'eid'=>$a['username'],'status'=>1])->find();if(!$m)$this->fail('请关联有效现场人员账号后参与盘点',403);try{return json(['code'=>0,'message'=>'成功','data'=>$work((int)$m['id'],$m)]);}catch(\InvalidArgumentException $e){$this->fail($e->getMessage());}});}
    private static function dateText($v){return $v?(new \DateTimeImmutable('@'.(int)$v))->setTimezone(new \DateTimeZone('Asia/Shanghai'))->format('m-d H:i'):'';}
    private static function filled($v){return $v!==null&&$v!=='';}
    private static function hash($r){ksort($r);return hash('sha256',json_encode(array_map(function($v){return $v===null?null:(string)$v;},$r)));}
    private function eventData(){ $r=Db::name('onsite_base')->where(['id'=>260,'mid'=>260])->find()?:[];return ['id'=>260,'title'=>$r['title']??'未设置盘点事件','status'=>(int)($r['mark']??0),'dates'=>$r['content']??'','version'=>self::hash($r)]; }
    private function arg($key,$default=null){return $this->request->method()==='POST'?$this->request->post($key,$default):$this->request->get($key,$default);}
    private function scope(){ $line=$this->arg('line');$rack=$this->arg('rack','');if(!is_scalar($line)||!ctype_digit((string)$line)||(int)$line<1||!is_string($rack)||mb_strlen($rack)>100)$this->fail('请选择有效产线和料架');return [(int)$line,$rack]; }
    private function phase(){ $p=$this->arg('phase','initial');if(!in_array($p,['initial','recheck'],true))$this->fail('盘点阶段无效');return $p; }
    private function tables(){Db::query('SELECT 1');$c=Db::connect()->getConnection();$p=$c->getConfig('prefix');if(!preg_match('/^[a-zA-Z0-9_]*$/D',$p))throw new \RuntimeException('Invalid prefix');return [$c->getPdo(),$p,$c->getConfig('database')];}
    private function locked($work){list($pdo,$p,$db)=$this->tables();$key='v2inventory:'.substr(hash('sha256',$db.$p),0,40);$q=$pdo->prepare('SELECT GET_LOCK(?,5)');$q->execute([$key]);if((int)$q->fetchColumn()!==1)$this->fail('其他盘点操作正在进行，请稍后重试',409);try{$pdo->exec("LOCK TABLES `{$p}onsite_inventory_record` WRITE, `{$p}onsite_inventory` READ, `{$p}onsite_base` READ");try{return $work($pdo,$p);}finally{$pdo->exec('UNLOCK TABLES');}}finally{$q=$pdo->prepare('SELECT RELEASE_LOCK(?)');$q->execute([$key]);}}
    private function recordsFor($ids){if(!$ids)return [];$rows=Db::name('onsite_inventory_record')->where('plan_id',260)->whereIn('i_id',$ids)->select()->toArray();$map=[];foreach($rows as $r){$id=$r['i_id'];if(isset($map[$id]))$map[$id]['duplicate']=true;else $map[$id]=$r;}return $map;}
    private static function slot($r,$mid){if(!$r)return 'num';if((int)$r['auditor']===$mid)return 'num';if((int)$r['auditor1']===$mid)return 'num1';if(!self::filled($r['num']))return 'num';if(!self::filled($r['num1']))return 'num1';return '';}
    private static function equal($a,$b){if(!self::filled($a)||!self::filled($b))return false;return self::decimal($a)===self::decimal($b);}
    private static function decimal($v){$v=(string)$v;if(!preg_match('/^[0-9]+(?:\.[0-9]+)?$/D',$v))return $v;$s=explode('.',$v);$i=ltrim($s[0],'0');$f=rtrim($s[1]??'','0');return ($i===''?'0':$i).($f!==''?'.'.$f:'');}
    private static function version($a,$r){return self::hash(['address'=>self::hash($a),'record'=>$r?self::hash($r):'']);}
    private function row($a,$r,$mid,$phase,$e){$slot=self::slot($r,$mid);$value=$r&&$slot!==''?($r[$slot]??''):'';$can=$e['status']===1&&$slot!=='';if($phase==='recheck'){$value=$r['check_num1']??'';$can=$e['status']===2&&$r&&(int)$r['recheck']===2&&!self::filled($value);}
        return ['id'=>(int)$a['id'],'recordId'=>(int)($r['id']??0),'location'=>$a['location'],'kanban'=>$a['kanban'],'unit'=>$a['unit_c'],'type'=>(int)$a['type'],'highlight'=>(int)$a['highlight'],'note'=>$a['mark']??'','value'=>$value??'','editable'=>$can&&empty($r['duplicate']),'duplicate'=>!empty($r['duplicate']),'version'=>self::version($a,$r),'initial1'=>$r['num']??'','initial2'=>$r['num1']??'','check1'=>$r['check_num1']??'','check2'=>$r['check_num2']??'','reason'=>$r['check_mark']??'','auditor'=>(int)($r['auditor']??0),'auditor1'=>(int)($r['auditor1']??0),'gap'=>$phase==='recheck'&&$r&&self::filled($r['check_num1'])&&!self::equal(self::filled($r['check_num2'])?$r['check_num2']:$r['check_num1'],$r['num'])];}
    public function context(){return $this->run('GET',function($mid,$m){$teams=Db::name('team_base')->whereIn('mid',[1,2])->column('title','id');$lines=Db::name('onsite_inventory')->field('project,line')->group('project,line')->order('project,line')->select()->toArray();$out=[];foreach($lines as $r)$out[]=['id'=>(int)$r['line'],'title'=>($teams[$r['project']]??$r['project']).' / '.($teams[$r['line']]??$r['line'])];$line=isset($m['tid'])?Db::name('team_list')->where('id',$m['tid'])->value('line'):0;return ['event'=>$this->eventData(),'lines'=>$out,'defaultLine'=>(int)$line,'memberId'=>$mid];});}
    public function racks(){return $this->run('GET',function($mid){list($line)=$this->scope();$phase=$this->phase();$e=$this->eventData();$as=Db::name('onsite_inventory')->where('line',$line)->field('id,rack')->select()->toArray();$rs=$this->recordsFor(array_column($as,'id'));$out=[];foreach($as as $a){$r=$rs[$a['id']]??null;if($phase==='recheck'&&(!$r||(int)$r['recheck']!==2))continue;$k=$a['rack'];if(!isset($out[$k]))$out[$k]=['rack'=>$k,'total'=>0,'registered'=>0,'complete'=>0];$out[$k]['total']++;if($phase==='recheck'){$done=$r&&self::filled($r['check_num1']);$out[$k]['registered']+=(int)$done;$out[$k]['complete']+=(int)$done;}else{$one=$r&&(self::filled($r['num'])||self::filled($r['num1']));$two=$r&&self::filled($r['num'])&&self::filled($r['num1']);$out[$k]['registered']+=(int)$one;$out[$k]['complete']+=(int)$two;}}uksort($out,'strnatcasecmp');return ['event'=>$e,'items'=>array_values($out)];});}
    public function detail(){return $this->run('GET',function($mid){list($line,$rack)=$this->scope();$phase=$this->phase();$e=$this->eventData();$q=Db::name('onsite_inventory')->where(['line'=>$line,'rack'=>$rack]);$all=(clone $q)->select()->toArray();$rs=$this->recordsFor(array_column($all,'id'));$summary=['total'=>0,'mine'=>0,'complete'=>0];$items=[];$kw=$this->request->get('keyword','');$method=$this->request->get('type','');$cursor=max(0,(int)$this->request->get('cursor',0));$zero=$this->request->get('zero','0')==='1';if(!is_string($kw)||mb_strlen($kw)>100||!in_array((string)$method,['','0','1','2'],true))$this->fail('筛选参数无效');
        foreach($all as $a){$r=$rs[$a['id']]??null;if($phase==='recheck'&&(!$r||(int)$r['recheck']!==2))continue;$row=$this->row($a,$r,$mid,$phase,$e);$summary['total']++;$summary['mine']+=(int)self::filled($row['value']);$summary['complete']+=(int)($phase==='recheck'?self::filled($r['check_num1']):($r&&self::filled($r['num'])&&self::filled($r['num1'])));if($zero){if($phase==='initial'&&$row['editable']&&$row['type']>0&&!self::filled($row['value']))$items[]=$row;continue;}if($a['id']<=$cursor||($method!==''&&(string)$a['type']!==(string)$method)||($kw!==''&&stripos($a['kanban'],$kw)===false&&stripos($a['location'],$kw)===false))continue;$items[]=$row;}
        usort($items,function($a,$b){return $a['id']<=>$b['id'];});$total=count($items);$items=array_slice($items,0,$zero?10000:50);$ids=[];foreach($items as $r){$ids[]=$r['auditor'];$ids[]=$r['auditor1'];}$names=$ids?Db::name('member_list')->whereIn('id',array_unique($ids))->column('ename','id'):[];foreach($items as &$r){$r['time1']=self::dateText($rs[$r['id']]['submitdate']??0);$r['time2']=self::dateText($rs[$r['id']]['submitdate1']??0);$r['people']=trim(($names[$r['auditor']]??'').' / '.($names[$r['auditor1']]??''),' /');}unset($r);return ['event'=>$e,'summary'=>$summary,'items'=>$items,'nextCursor'=>$total>count($items)?(int)end($items)['id']:null];});}
    private function quantity($v,$type,$phase){if($type===0){if($v!=='Y')$this->fail('U盘完成时必须为Y');return 'Y';}if(!is_scalar($v)||!preg_match('/^(0|[1-9][0-9]{0,11})(\.[0-9]{1,6})?$/D',(string)$v))$this->fail('数量须为非负数，最多12位整数和6位小数');$v=self::decimal($v);if($phase==='recheck'&&strlen($v)>10)$this->fail('复盘数量超过原字段10字符容量');return $v;}
    public function save(){return $this->run('POST',function($mid){list($line,$rack)=$this->scope();$phase=$this->phase();$d=$this->request->post();$items=$d['items']??null;if(!is_array($items)||!$items||count($items)>10000)$this->fail('请提交1至10000项修改');return $this->locked(function($pdo,$p)use($d,$items,$mid,$line,$rack,$phase){$e=$this->eventData();if(!is_string($d['eventVersion']??null)||!hash_equals($e['version'],$d['eventVersion'])||$e['status']!==($phase==='initial'?1:2))$this->fail('盘点阶段或事件已变化，请返回列表刷新',409);$addresses=Db::name('onsite_inventory')->where(['line'=>$line,'rack'=>$rack])->select()->toArray();$as=array_column($addresses,null,'id');$rs=$this->recordsFor(array_keys($as));$changes=[];$seen=[];$conflicts=[];
        foreach($items as $v){if(!is_array($v)||!isset($v['id'])||!is_scalar($v['id'])||!ctype_digit((string)$v['id'])||isset($seen[$v['id']])||!isset($as[$v['id']]))$this->fail('物料不属于本料架或重复提交');$id=(int)$v['id'];$seen[$id]=true;$a=$as[$id];$r=$rs[$id]??null;if(!empty($r['duplicate'])){$conflicts[]=$id;continue;}$value=$this->quantity($v['value']??null,(int)$a['type'],$phase);$row=$this->row($a,$r,$mid,$phase,$e);$gap=($v['action']??'')==='gap';$owned=$r&&($phase==='initial'?((int)$r['auditor']===$mid||(int)$r['auditor1']===$mid):((int)($r['check_auditor1']??0)===$mid));
            if(!$gap&&$owned&&self::equal($row['value'],$value))continue;
            if(!is_string($v['version']??null)||!hash_equals($row['version'],$v['version'])||(!$gap&&!$row['editable'])){$conflicts[]=$id;continue;}
            if($gap){if($phase!=='recheck'||!$row['gap'])$this->fail('仅差异复盘项可以再次确认');$reason=$v['reason']??'';if(!is_string($reason)||trim($reason)===''||mb_strlen($reason)>255)$this->fail('请填写255字以内的异常原因');$r['check_num2']=$value;$r['check_auditor2']=$mid;$r['check_mark']=trim($reason);}
            elseif($phase==='recheck'){if(!$r||(int)$r['recheck']!==2)$this->fail('该物料不在复盘名单');$r['check_num1']=$value;$r['check_auditor1']=$mid;}
            else{$slot=self::slot($r,$mid);if(!$r)$r=['id'=>null,'plan_id'=>260,'i_id'=>$id,'project'=>$a['project'],'line'=>$a['line'],'kanban'=>$a['kanban'],'num'=>null,'auditor'=>null,'submitdate'=>null,'num1'=>null,'auditor1'=>null,'submitdate1'=>null,'mark'=>null,'status'=>1,'recheck'=>1,'check_num1'=>null,'check_auditor1'=>null,'check_num2'=>null,'check_auditor2'=>null,'check_mark'=>null];$r[$slot]=$value;$r[$slot==='num'?'auditor':'auditor1']=$mid;$r[$slot==='num'?'submitdate':'submitdate1']=time();if((int)$a['type']!==1){$r['num']=$value;$r['num1']=$value;$r['auditor']=$mid;$r['auditor1']=$mid;$r['submitdate']=time();$r['submitdate1']=time();}$r['status']=self::filled($r['num'])&&self::filled($r['num1'])?2:1;}$changes[]=$r;
        }
        if($conflicts)return ['saved'=>0,'conflicts'=>$conflicts];
        if($phase==='recheck'&&!array_filter($items,function($v){return ($v['action']??'')==='gap';})){$next=$rs;foreach($changes as $r)$next[$r['i_id']]=$r;foreach($next as $r)if((int)$r['recheck']===2&&!self::filled($r['check_num1']))$this->fail('本料架尚未全部复盘，请填完后再提交');}
        if($changes){$fields=array_keys($changes[0]);$groups=[];$values=[];foreach($changes as $r){$groups[]='('.implode(',',array_fill(0,count($fields),'?')).')';foreach($fields as $f)$values[]=$r[$f]??null;}$cols=implode(',',array_map(function($f){return '`'.$f.'`';},$fields));$updates=[];foreach($fields as $f)if($f!=='id')$updates[]="`$f`=VALUES(`$f`)";$sql="INSERT INTO `{$p}onsite_inventory_record` ($cols) VALUES ".implode(',',$groups).' ON DUPLICATE KEY UPDATE '.implode(',',$updates);$q=$pdo->prepare($sql);$q->execute($values);}return ['saved'=>count($changes),'conflicts'=>[]];});});}
    public function mine(){return $this->run('GET',function($mid){$page=max(1,min(100000,(int)$this->request->get('page',1)));$q=Db::name('onsite_inventory_record')->where('plan_id',260)->where(function($q)use($mid){$q->where('auditor',$mid)->whereOr('auditor1',$mid)->whereOr('check_auditor1',$mid)->whereOr('check_auditor2',$mid);});$total=(clone $q)->count();$rs=$q->order('id desc')->limit(($page-1)*30,30)->select()->toArray();$as=$rs?Db::name('onsite_inventory')->whereIn('id',array_column($rs,'i_id'))->select()->toArray():[];$as=array_column($as,null,'id');$out=[];foreach($rs as $r){$a=$as[$r['i_id']]??[];$out[]=['id'=>(int)$r['id'],'rack'=>$a['rack']??'地址缺失','location'=>$a['location']??'','kanban'=>$r['kanban'],'time'=>self::dateText((int)$r['auditor']===$mid?$r['submitdate']:((int)$r['auditor1']===$mid?$r['submitdate1']:0)),'value'=>(int)$r['auditor']===$mid?$r['num']:((int)$r['auditor1']===$mid?$r['num1']:''),'check1'=>(int)$r['check_auditor1']===$mid?$r['check_num1']:'','check2'=>(int)$r['check_auditor2']===$mid?$r['check_num2']:''];}return ['items'=>$out,'total'=>$total];});}
}
