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

class V2InventoryRecord extends V2Base
{
    private function run($work){try{return json(['code'=>0,'data'=>$work()]);}catch(\InvalidArgumentException $e){$this->fail($e->getMessage());}}
    private function admin(){if((int)($this->actor['rightid']??0)<=98)$this->fail('仅管理员可管理盘点事件或清理盘点记录',403);}
    private static function date($value){if(!is_string($value))throw new \InvalidArgumentException('日期格式无效');$d=\DateTimeImmutable::createFromFormat('!Y-m-d H:i:s',$value,new \DateTimeZone('Asia/Shanghai'));if(!$d||$d->format('Y-m-d H:i:s')!==$value||$d->getTimestamp()<1||$d->getTimestamp()>2147483647)throw new \InvalidArgumentException('日期格式无效或超出支持范围');return $d->getTimestamp();}
    private static function range($value,$optional=false){if($optional&&($value===null||$value===[]))return [null,null];if(!is_array($value)||count($value)!==2)throw new \InvalidArgumentException('请选择完整起止时间');$a=self::date($value[0]);$b=self::date($value[1]);if($b<$a)throw new \InvalidArgumentException('结束时间不能早于开始时间');return [$a,$b];}
    private static function format($time){return $time?(new \DateTimeImmutable('@'.(int)$time))->setTimezone(new \DateTimeZone('Asia/Shanghai'))->format('Y-m-d H:i:s'):'';}
    private static function rawEvent(){return Db::name('onsite_base')->where(['id'=>260,'mid'=>260])->find()?:[];}
    public function event(){return $this->run(function(){return $this->eventData();});}
    private function eventData(){
        $e=self::rawEvent();$dates=explode('~',$e['content']??'');$range=count($dates)===2&&ctype_digit($dates[0])&&ctype_digit($dates[1])?[self::format($dates[0]),self::format($dates[1])]:[];$p=V2InventoryData::window($e);
        return ['title'=>$e['title']??'','status'=>(int)($e['mark']??0),'dates'=>$range,'freeze'=>$p['start']?[$p['start'],$p['end']]:[],'policy'=>$p,'version'=>V2InventoryData::version($e),'canManage'=>(int)($this->actor['rightid']??0)>98];
    }
    public function saveEvent(){return $this->run(function(){
        $this->admin();$d=$this->request->post();$title=$d['title']??null;$status=$d['status']??null;if(!is_string($title)||trim($title)===''||mb_strlen($title)>100||!in_array($status,[0,1,2],true))throw new \InvalidArgumentException('请填写事件名称并选择初盘、复盘或结束');$range=self::range($d['dates']??null);$freeze=self::range($d['freeze']??null,true);
        if($status!==0&&$freeze[0]===null)throw new \InvalidArgumentException('初盘或复盘必须明确设置禁止更新起止时间');
        return V2InventoryData::lock(function($pdo,$prefix)use($d,$title,$status,$range,$freeze){$pdo->exec('LOCK TABLES '.V2InventoryData::quote($prefix.'onsite_base').' WRITE');try{$e=self::rawEvent();if(!is_string($d['version']??null)||!hash_equals(V2InventoryData::version($e),$d['version']))throw new \InvalidArgumentException('盘点事件已变化，请重新打开');$values=['title'=>trim($title),'mark'=>$status,'content'=>implode('~',$range),'freeze_start'=>$freeze[0],'freeze_end'=>$freeze[1]];
            if($e)Db::name('onsite_base')->where(['id'=>260,'mid'=>260])->update($values);else {if(Db::name('onsite_base')->where('id',260)->count())throw new \InvalidArgumentException('事件ID被其他配置占用');Db::name('onsite_base')->insert(array_merge($values,['id'=>260,'mid'=>260]));}return $this->eventData();}finally{$pdo->exec('UNLOCK TABLES');}});
    });}
    private function query(){
        $q=Db::name('onsite_inventory_record')->alias('r')->leftJoin('onsite_inventory a','a.id=r.i_id');
        foreach(['project','line'] as $f){$v=$this->request->get($f,'');if(!is_scalar($v)||($v!==''&&!ctype_digit((string)$v)))throw new \InvalidArgumentException('筛选参数无效');if($v!=='')$q->where('r.'.$f,(int)$v);}
        foreach(['rack','keyword'] as $f){$v=$this->request->get($f,'');if(!is_string($v)||mb_strlen($v)>100)throw new \InvalidArgumentException('筛选参数无效');if($v!=='')$q->whereLike($f==='rack'?'a.rack':'r.kanban','%'.addcslashes(trim($v),'%_\\').'%');}
        $type=$this->request->get('scope','');if(!in_array($type,['','different','recheck'],true))throw new \InvalidArgumentException('记录范围无效');if($type==='different')$q->whereRaw('r.num IS NOT NULL AND r.num1 IS NOT NULL AND CAST(r.num AS DECIMAL(20,6))<>CAST(r.num1 AS DECIMAL(20,6))');if($type==='recheck')$q->where('r.recheck',2);return $q;
    }
    public static function itemType($a){foreach(['super'=>'超级看板','compose'=>'组合卡丁','pull_line'=>'拉力线','copper'=>'废铜片','wire'=>'废导线'] as $f=>$label)if((int)($a[$f]??0)===1)return $label;return '普通物料';}
    private static function decorate($records){
        if(!$records)return [];$addresses=Db::name('onsite_inventory')->whereIn('id',array_column($records,'i_id'))->select()->toArray();$addresses=array_column($addresses,null,'id');$teams=Db::name('team_base')->whereIn('mid',[1,2])->column('title','id');$people=[];
        foreach($records as $r)foreach(['auditor','auditor1','check_auditor1','check_auditor2'] as $f)if(!empty($r[$f]))$people[]=$r[$f];$names=$people?Db::name('member_list')->whereIn('id',array_unique($people))->column('ename','id'):[];
        foreach($records as &$r){$r['version']=V2InventoryData::version($r);$a=$addresses[$r['i_id']]??[];$r['addressMissing']=!$a;$r['projectName']=$teams[$r['project']]??'项目ID '.$r['project'];$r['lineName']=$teams[$r['line']]??'产线ID '.$r['line'];foreach(['rack','location','unit_c','type'] as $f)$r[$f]=$a[$f]??'';$r['itemType']=self::itemType($a);$r['method']=['0'=>'U盘','1'=>'计数','2'=>'称重'][(string)($a['type']??'')]??'未定义';foreach(['auditor','auditor1','check_auditor1','check_auditor2'] as $f)$r[$f.'Name']=empty($r[$f])?'':($names[$r[$f]]??'人员ID '.$r[$f]);foreach(['submitdate','submitdate1'] as $f)$r[$f.'Text']=self::format($r[$f]??0);}unset($r);return $records;
    }
    public function index(){return $this->run(function(){$page=max(1,min(100000,(int)$this->request->get('page',1)));$limit=max(1,min(100,(int)$this->request->get('limit',30)));$q=$this->query();$total=(clone $q)->count();$rows=$q->field('r.*')->order('r.id desc')->limit(($page-1)*$limit,$limit)->select()->toArray();return ['list'=>self::decorate($rows),'total'=>$total];});}
    private static function quantity($v){if($v==='Y')return 'Y';if($v===null||$v==='')return null;if(!is_scalar($v)||!preg_match('/^(0|[1-9][0-9]{0,11})(\.[0-9]{1,6})?$/D',(string)$v))throw new \InvalidArgumentException('数量须为非负数，最多12位整数及6位小数；空白表示未盘点');return (string)$v;}
    private static function lockedRecords($work){return V2InventoryData::lock(function($pdo,$prefix)use($work){$names=['onsite_inventory_record'=>'WRITE','onsite_inventory'=>'READ','onsite_base'=>'READ'];$parts=[];foreach($names as $t=>$mode)$parts[]=V2InventoryData::quote($prefix.$t).' '.$mode;$pdo->exec('LOCK TABLES '.implode(',',$parts));try{return $work($pdo,$prefix);}finally{$pdo->exec('UNLOCK TABLES');}});}
    public function update(){return $this->run(function(){
        $d=$this->request->post();$id=filter_var($d['id']??null,FILTER_VALIDATE_INT,['options'=>['min_range'=>1]]);if(!$id)throw new \InvalidArgumentException('记录ID无效');$num=self::quantity($d['num']??null);$num1=self::quantity($d['num1']??null);
        return self::lockedRecords(function($pdo,$prefix)use($d,$id,$num,$num1){$q=$pdo->prepare('SELECT * FROM '.V2InventoryData::quote($prefix.'onsite_inventory_record').' WHERE id=?');$q->execute([$id]);$r=$q->fetch(\PDO::FETCH_ASSOC);$q=$pdo->query('SELECT * FROM '.V2InventoryData::quote($prefix.'onsite_base').' WHERE id=260 AND mid=260');$event=$q->fetch(\PDO::FETCH_ASSOC);if(!$event||(int)$event['mark']!==1||!$r||(int)$r['plan_id']!==260)throw new \InvalidArgumentException('仅能在初盘事件中修改当前事件的盘点数量');if(!is_string($d['version']??null)||!hash_equals(V2InventoryData::version($r),$d['version']))throw new \InvalidArgumentException('记录已被盘点人员更新，请刷新后重试');
            $address=Db::name('onsite_inventory')->where('id',$r['i_id'])->find();if(!$address)throw new \InvalidArgumentException('关联地址缺失，无法编辑');foreach(['num'=>$num,'num1'=>$num1] as $field=>$value){if($value==='Y'&&(int)$address['type']!==0)throw new \InvalidArgumentException('只有U盘项目可填写Y');if((int)$address['type']===0&&$value!==null&&$value!=='Y'&&$value!==(string)($r[$field]??''))throw new \InvalidArgumentException('U盘完成请填写Y；空白表示未盘点');}
            $changes=[];foreach(['num'=>$num,'num1'=>$num1] as $f=>$v)if(($r[$f]===null?null:(string)$r[$f])!==$v)$changes[]=($f==='num'?'数量1':'数量2').'：'.($r[$f]??'未盘点').'→'.($v??'未盘点');if(!$changes)return ['saved'=>false];$audit="\n".$this->actor['truename'].'于'.self::format(time()).'修改'.implode('；',$changes);$mark=(string)$r['mark'].$audit;if(mb_strlen($mark)>500)throw new \InvalidArgumentException('修改历史已达到备注容量，请先核对并归档，禁止截断历史');$q=$pdo->prepare('UPDATE '.V2InventoryData::quote($prefix.'onsite_inventory_record').' SET num=?,num1=?,mark=? WHERE id=?');$q->execute([$num,$num1,$mark,$id]);return ['saved'=>true];});
    });}
    private static function recheckRows($ids,$pdo,$prefix){
        if(!is_array($ids)||!$ids||count($ids)>10000)throw new \InvalidArgumentException('复盘名单须为1至10000个SN');foreach($ids as $id)if(!is_scalar($id)||!ctype_digit((string)$id)||(int)$id<1)throw new \InvalidArgumentException('SN须为地址正整数ID');$ids=array_values(array_unique(array_map('intval',$ids)));$records=[];
        $event=$pdo->query('SELECT mark FROM '.V2InventoryData::quote($prefix.'onsite_base').' WHERE id=260 AND mid=260')->fetchColumn();if(!in_array((int)$event,[1,2],true))throw new \InvalidArgumentException('请先设置初盘或复盘事件');
        foreach(array_chunk($ids,500) as $chunk){$q=$pdo->query('SELECT * FROM '.V2InventoryData::quote($prefix.'onsite_inventory_record').' WHERE plan_id=260 AND i_id IN('.implode(',',$chunk).') ORDER BY id');foreach($q->fetchAll(\PDO::FETCH_ASSOC) as $r)$records[$r['i_id']][]=$r;}
        $out=[];$errors=[];foreach($ids as $id){$rows=$records[$id]??[];if(count($rows)!==1){$errors[]='SN '.$id.'：'.(!$rows?'无初盘记录':'存在重复记录');continue;}$q=$pdo->query('SELECT type FROM '.V2InventoryData::quote($prefix.'onsite_inventory').' WHERE id='.$id);$type=$q->fetchColumn();if($type===false||(string)$type!=='1'){$errors[]='SN '.$id.'：不是计数地址或地址缺失';continue;}$out[]=$rows[0];}if($errors)throw new \InvalidArgumentException(implode('；',array_slice($errors,0,30)));return $out;
    }
    public function recheckPreview(){return $this->run(function(){return self::lockedRecords(function($pdo,$prefix){$rows=self::recheckRows($this->request->post('ids'),$pdo,$prefix);return ['count'=>count($rows),'version'=>hash('sha256',json_encode($rows))];});});}
    public function recheck(){return $this->run(function(){return self::lockedRecords(function($pdo,$prefix){$rows=self::recheckRows($this->request->post('ids'),$pdo,$prefix);$version=$this->request->post('version');if(!is_string($version)||!hash_equals(hash('sha256',json_encode($rows)),$version))throw new \InvalidArgumentException('盘点记录已变化，请重新检查复盘名单');$ids=array_column($rows,'id');$changed=$pdo->exec('UPDATE '.V2InventoryData::quote($prefix.'onsite_inventory_record').' SET recheck=2 WHERE id IN('.implode(',',$ids).')');return ['count'=>count($rows),'changed'=>$changed];});});}
    private static function maintenanceState($pdo,$prefix,$operation){
        if(!in_array($operation,['duplicates','clear'],true))throw new \InvalidArgumentException('清理类型无效');$event=self::rawEvent();$table=$prefix.'onsite_inventory_record';$rows=$pdo->query('SELECT * FROM '.V2InventoryData::quote($table).' ORDER BY id DESC')->fetchAll(\PDO::FETCH_ASSOC);$seen=[];$ids=[];$conflicts=0;
        foreach($rows as $r){$key=$r['plan_id'].':'.$r['i_id'];$same=$r;unset($same['id']);if(isset($seen[$key])){if($seen[$key]===$same)$ids[]=(int)$r['id'];else $conflicts++;}else $seen[$key]=$same;}
        if($operation==='clear')$ids=array_map('intval',array_column($rows,'id'));return ['count'=>count($ids),'conflicts'=>$conflicts,'version'=>hash('sha256',json_encode([$rows,$event,$operation])),'ids'=>$ids,'ended'=>(int)($event['mark']??0)===0];
    }
    public function maintenancePreview(){return $this->run(function(){$this->admin();return V2InventoryData::lock(function($pdo,$prefix){$state=self::maintenanceState($pdo,$prefix,$this->request->post('operation'));unset($state['ids']);return $state;});});}
    public function maintenance(){return $this->run(function(){
        $this->admin();$d=$this->request->post();if(($d['confirmation']??'')!=='确认清理')throw new \InvalidArgumentException('请输入确认清理');return V2InventoryData::lock(function($pdo,$prefix)use($d){
            $backup=$prefix.'v2inv_records_backup';$records=$prefix.'onsite_inventory_record';$pdo->exec('CREATE TABLE IF NOT EXISTS '.V2InventoryData::quote($backup).' LIKE '.V2InventoryData::quote($records));$pdo->exec('LOCK TABLES '.V2InventoryData::quote($records).' WRITE,'.V2InventoryData::quote($backup).' WRITE,'.V2InventoryData::quote($prefix.'onsite_base').' READ');
            try{$s=self::maintenanceState($pdo,$prefix,$d['operation']??null);if(!$s['ended'])throw new \InvalidArgumentException('请先将盘点事件设为结束，再清理记录');if(!is_string($d['version']??null)||!hash_equals($s['version'],$d['version']))throw new \InvalidArgumentException('记录已变化，请重新检查清理范围');if(!$s['ids'])return ['removed'=>0];$pdo->exec('DELETE FROM '.V2InventoryData::quote($backup));$pdo->exec('INSERT INTO '.V2InventoryData::quote($backup).' SELECT * FROM '.V2InventoryData::quote($records));$removed=$pdo->exec('DELETE FROM '.V2InventoryData::quote($records).($d['operation']==='clear'?'':' WHERE id IN('.implode(',',$s['ids']).')'));return ['removed'=>$removed];}finally{$pdo->exec('UNLOCK TABLES');}
        });
    });}
    public static function expand($graph,$project,$kanban,$factor=1,$path=[]){
        $key=$project.':'.$kanban;if(isset($path[$key])||count($path)>=30)throw new \InvalidArgumentException('BOM存在循环或层级超过30：'.$kanban);$path[$key]=true;$out=[];
        foreach($graph[$key]??[] as $r){$n=$factor*(float)$r['num'];$child=$project.':'.$r['children'];if(isset($graph[$child]))$out=array_merge($out,self::expand($graph,$project,$r['children'],$n,$path));else $out[]=['kanban'=>$r['children'],'num'=>round($n,8),'unit'=>$r['unit']];if(count($out)>10000)throw new \InvalidArgumentException('单个BOM展开超过10000行');}return $out;
    }
    public function export(){return $this->run(function(){
        $type=$this->request->get('type');if(!in_array((string)$type,['1','2'],true))throw new \InvalidArgumentException('请选择初盘或复盘');list($pdo,$prefix)=V2InventoryData::connection();$tables=V2InventoryData::live($prefix);$tables['records']=$prefix.'onsite_inventory_record';$stamp=V2InventoryData::fingerprint($pdo,$tables);$previous=$this->request->get('version');if($previous!==null&&(!is_string($previous)||!hash_equals($stamp,$previous)))throw new \InvalidArgumentException('导出期间盘点数据已变化，请重新导出');$upper=$this->request->get('upperId');$upper=$upper===null?(int)Db::name('onsite_inventory_record')->max('id'):(int)$upper;$cursor=max(0,(int)$this->request->get('cursor',0));$q=Db::name('onsite_inventory_record')->where('id','>',$cursor)->where('id','<=',$upper);if((string)$type==='2')$q->where('recheck',2);$raw=$q->order('id')->limit(200)->select()->toArray();$rows=self::decorate($raw);$graph=[];if((string)$type==='1')foreach(Db::name('onsite_inventory_super')->select()->toArray() as $b)$graph[$b['project'].':'.$b['kanban']][]=$b;$out=[];
        foreach($rows as $r){if($r['addressMissing'])throw new \InvalidArgumentException('记录ID '.$r['id'].'关联地址缺失，无法完整导出');if((string)$type==='2'){$out[]=['序号'=>$r['id'],'ID'=>(string)$r['i_id'],'项目'=>$r['projectName'],'产线'=>$r['lineName'],'工装架'=>$r['rack'],'位置'=>$r['location'],'料号'=>$r['kanban'],'单位'=>$r['unit_c'],'盘点1'=>$r['num'],'盘点2'=>$r['num1'],'复盘1'=>$r['check_num1'],'复盘2'=>$r['check_num2'],'备注'=>$r['check_mark']];continue;}
            $v=['SN'=>(string)$r['i_id'],'项目'=>$r['projectName'],'产线'=>$r['lineName'],'位置'=>$r['rack'].'-'.$r['location'],'kanban'=>$r['kanban'],'单位'=>$r['unit_c'],'盘点1'=>$r['num'],'盘点2'=>$r['num1'],'看板类型'=>$r['itemType']==='普通物料'?'':$r['itemType'],'子看板'=>'','盘点类型'=>$r['method'],'备注'=>$r['mark']];
            if($r['num']!=='Y'&&in_array($r['itemType'],['超级看板','组合卡丁'],true)){$bom=self::expand($graph,$r['project'],$r['kanban']);if($bom&&(float)$r['num']>0){$parent=$v;$parent['盘点1']='';$parent['子看板']='已炸开';$out[]=$parent;foreach($bom as $b){$child=$v;$child['位置']=$r['kanban'];$child['kanban']=$b['kanban'];$child['盘点1']=round((float)$r['num']*$b['num'],8);$child['盘点2']='';$child['单位']=$b['unit'];$child['看板类型']='子看板';$out[]=$child;}}else{$v['子看板']='系统无子看板或盘点数量为0';$out[]=$v;}}else $out[]=$v;
        }
        if(!hash_equals($stamp,V2InventoryData::fingerprint($pdo,$tables)))throw new \InvalidArgumentException('导出期间数据已变化，请重试');return ['list'=>$out,'upperId'=>$upper,'version'=>$stamp,'nextCursor'=>count($raw)===200?(int)end($raw)['id']:null];
    });}
}
