<?php
namespace app\mobile\controller;

use think\facade\Db;
use think\facade\Filesystem;
use think\facade\Log;
use app\mobile\service\OutplantApprovers;

/** 物品出厂，复用原业务表；人员出厂、门岗继续使用原流程。 */
class OmsOutplant extends OmsBase
{
    public function messageItems($account,$since,\app\mobile\service\MessageInbox $inbox) {
        if(!$inbox->allowsSource($this))$this->fail('接口不存在',404);
        if(!(int)$account['admin_id'])return [];$aid=(int)$account['admin_id'];
        $rows=Db::name('common_outplant')->where('type',2)->where(function($q)use($aid,$since){
            $q->where('on_check',$aid)->whereOr(function($own)use($aid,$since){$own->where('owner',$aid)->where('ownerType','>',1)->whereIn('status',[0,4,5])->where('create_time','>=',$since);});
            foreach([1,2,3] as $stage)$q->whereOr(function($done)use($aid,$since,$stage){$done->where('apv'.$stage,$aid)->where('apv'.$stage.'_time','>=',$since);});
        })->select()->toArray();$items=[];
        foreach($rows as $row){$p=$this->present($row,$account);$fields=['申请人'=>$p['ownerName'],'出厂原因'=>$p['reason'],'出厂时间'=>$p['outTime']];$route='/pages/outplant/detail?id='.$row['id'];
            $arrival=(int)$row['create_time'];foreach([1=>'主管',2=>'经理',3=>'财务'] as $i=>$label){
                if((int)$row['apv'.$i]===$aid && (int)$row['apv'.$i.'_time']>0)$items[]=\app\mobile\service\MessageInbox::item('outplant',$row['id'],'approve-'.$i,'出厂审批已处理',$row['apv'.$i.'_mark']?'已驳回':'已通过',$fields,$route,'done',$row['apv'.$i.'_time']);
                if($i<(int)$row['status'])$arrival=max($arrival,(int)$row['apv'.$i.'_time']);
            }
            if($p['canApprove'])$items[]=\app\mobile\service\MessageInbox::item('outplant',$row['id'],'approve-'.$row['status'],'物品出厂待审批',$p['statusName'],$fields,$route,'pending',$arrival);
            if((int)$row['owner']===$aid && (int)$row['ownerType']>1 && in_array((int)$row['status'],[0,4,5],true))$items[]=\app\mobile\service\MessageInbox::item('outplant',$row['id'],'result-'.$row['status'],'物品出厂'.$p['statusName'],$p['statusName'],$fields,$route,'notice',max($arrival,(int)$row['check_time']));
        }return $items;
    }
    private function result($data = null) { return json(['code'=>0, 'message'=>'成功', 'data'=>$data]); }
    private function integer($value, $label) {
        if (!is_scalar($value) || !preg_match('/^[1-9][0-9]{0,9}$/', (string)$value) || (float)$value > 2147483647) $this->fail($label.'不正确');
        return (int)$value;
    }
    private function text($value, $max, $label, $optional = false) {
        if (!is_string($value) || mb_strlen($value) > $max || (!$optional && trim($value)==='')) $this->fail($label.'不正确');
        return trim($value);
    }
    private function admin($account) {
        $admin = (int)$account['admin_id'] > 0 ? Db::name('admin_user')->where(['id'=>$account['admin_id'], 'status'=>1, 'username'=>$account['username']])->find() : null;
        if (!$admin) $this->fail('物品出厂需要有效的后台人员关联', 403);
        return $admin;
    }
    private function owned($row, $account) { return (int)$row['ownerType'] > 1 && (int)$row['owner'] === (int)$account['admin_id']; }
    private function permissions($row, $account) {
        $stage = (int)$row['status'];
        $consistent = true;
        if ($stage >= 1 && $stage <= 3) foreach ([1,2,3] as $i) {
            if ((int)$row['apv'.$i] > 0 && (($i < $stage && empty($row['apv'.$i.'_time'])) || ($i >= $stage && !empty($row['apv'.$i.'_time'])))) $consistent = false;
        }
        return [
            'canApprove'=>$consistent && $stage>=1 && $stage<=3 && (int)$row['on_check']===(int)$account['admin_id'] && (int)$row['apv'.$stage]===(int)$account['admin_id'],
            'canDelete'=>$this->owned($row,$account) && $stage===1 && empty($row['apv1_time']) && empty($row['apv2_time']) && empty($row['apv3_time']),
        ];
    }
    private function row($account, $id) {
        $row=Db::name('common_outplant')->where(['id'=>$this->integer($id,'申请编号'), 'type'=>2])->where('status','>=',0)->find();
        if (!$row) $this->fail('申请不存在或已删除',404);
        if (!$this->owned($row,$account) && !in_array((int)$account['admin_id'],array_map('intval',[$row['apv1'],$row['apv2'],$row['apv3']]),true)) $this->fail('无权查看此申请',403);
        return $row;
    }
    private function image($path) {
        $path=str_replace('\\','/',(string)$path);
        return strpos($path,'/storage/')===0 && strpos($path,'..')===false ? 'https://www.cqoms.com'.$path : '';
    }
    private function present($row,$account,$detail=false) {
        $names=Db::name('admin_user')->whereIn('id',array_filter([$row['owner'],$row['apv1'],$row['apv2'],$row['apv3'],$row['on_check']]))->column('truename','id');
        $date=function($value) { return $value>0 ? date('Y-m-d H:i',(int)$value) : ''; };
        $status=(int)$row['status'];
        $result=['id'=>(int)$row['id'],'status'=>$status,'statusName'=>[0=>'已驳回',1=>'待主管审批',2=>'待经理审批',3=>'待财务审批',4=>empty($row['check_time'])?'审批通过 · 待放行':'已放行',5=>'已作废'][$status] ?? '历史状态',
            'ownerName'=>$names[$row['owner']] ?? '人员已变更','reason'=>(string)$row['reason'],'createdAt'=>$date($row['create_time']),'outTime'=>$date($row['out_time']), 'releaseTime'=>$date($row['check_time']),
            'currentApprover'=>$names[$row['on_check']] ?? '', 'checkType'=>(int)$row['checkType'],'backType'=>(int)$row['backType'],'express'=>(int)$row['express'],'expressCode'=>(string)$row['expressCode'],'approvalImage'=>$this->image($row['img']), 'materials'=>[], 'stages'=>[]];
        $result['returnStatus']=(int)($row['return_status'] ?? ((int)$row['backType']===1?3:1));
        $result['returnStatusName']=(int)$row['backType']===1?'无需返厂':([1=>'待返厂',2=>'部分返厂',3=>'返厂完成'][$result['returnStatus']] ?? '待核对');
        if ($detail) {
            $result['returnSummary']=(new OmsMaterialReturn($this->app))->summaryFor($row,$account);
            foreach (Db::name('common_outplant_material')->where(['n_id'=>$row['id'],'item'=>1])->order('id')->select()->toArray() as $material) {
                $result['materials'][]=['id'=>(int)$material['id'],'type'=>(int)$material['type'],'name'=>$material['name'],'unit'=>$material['unit'],'num'=>$material['num'],'mark'=>(string)$material['mark'],'image'=>$this->image($material['img'])];
            }
            foreach ([1=>'主管',2=>'经理',3=>'财务'] as $i=>$label) if ((int)$row['apv'.$i]>0) $result['stages'][]=['label'=>$label,'name'=>$names[$row['apv'.$i]] ?? '人员已变更','time'=>$date($row['apv'.$i.'_time']),'reason'=>(string)$row['apv'.$i.'_mark']];
        }
        return array_merge($result,$this->permissions($row,$account));
    }
    public function options() {
        return $this->guarded(function() {
            $this->requireMethod('GET');$account=$this->current();$admin=$this->admin($account);
            $departments=Db::name('admin_org')->column('title','id');$approvers=[];
            if ((int)$admin['dpt'] > 0) foreach(Db::name('admin_user')->where(['status'=>1,'dpt'=>$admin['dpt']])->field('id,truename,dpt')->order('job,id')->select()->toArray() as $person) $approvers[]=['id'=>(int)$person['id'],'name'=>$person['truename'],'departmentId'=>(int)$person['dpt'],'departmentName'=>$departments[$person['dpt']] ?? '未设置部门'];
            $scrap=OutplantApprovers::candidates(21);$scrapId=$scrap ? $scrap[0] : 0;
            return $this->result(['approvers'=>$approvers,'scrapApproverId'=>$scrapId,'scrapApproverName'=>$scrapId ? Db::name('admin_user')->where('id',$scrapId)->value('truename') : '未设置废品审批人']);
        });
    }
    public function records() {
        return $this->guarded(function() {
            $this->requireMethod('GET');$account=$this->current();$this->admin($account);$page=$this->integer($this->request->get('page',1),'页码');
            $query=Db::name('common_outplant')->where('type',2)->where('status','>=',0);$scope=$this->request->get('scope','mine');
            if ($scope==='mine') $query->where('owner',$account['admin_id'])->where('ownerType','>',1);
            elseif ($scope==='approve') $query->where(function($q) use($account){$q->where('apv1',$account['admin_id'])->whereOr('apv2',$account['admin_id'])->whereOr('apv3',$account['admin_id']);});
            else $this->fail('列表类型不正确');
            $total=(clone $query)->count();$rows=$query->order('id desc')->page($page,10)->select()->toArray();
            return $this->result(['items'=>array_map(function($row) use($account){return $this->present($row,$account);},$rows),'total'=>$total,'page'=>$page]);
        });
    }
    public function detail() {
        return $this->guarded(function() {$this->requireMethod('GET');$account=$this->current();$this->admin($account);return $this->result($this->present($this->row($account,$this->request->get('id')),$account,true));});
    }
    protected function stagingDirectory() { return $this->app->getRuntimePath().'outplant-uploads/'; }
    public function upload() {
        return $this->guarded(function() {
            $this->requireMethod('POST');$account=$this->current();$this->admin($account);$kind=$this->request->post('kind');
            if (!in_array($kind,['material','approval'],true)) $this->fail('图片用途不正确');
            $file=$this->request->file('image');
            if (!$file instanceof \think\file\UploadedFile || !$file->isValid() || $file->getSize()>5*1024*1024) $this->fail('请选择不超过5MB的图片');
            $types=['image/jpeg'=>'jpg','image/png'=>'png','image/webp'=>'webp'];$mime=$file->getMime();
            if (!isset($types[$mime]) || !@getimagesize($file->getPathname())) $this->fail('请选择JPG、PNG或WebP图片');
            $directory=$this->stagingDirectory();
            if (!is_dir($directory) && !mkdir($directory,0700,true) && !is_dir($directory)) $this->fail('图片暂存失败',503);
            foreach(glob($directory.'*') ?: [] as $old) if (preg_match('/^[a-f0-9]{48}\.(jpg|png|webp)$/',basename($old)) && is_file($old) && filemtime($old)<time()-86400) @unlink($old);
            $reference=bin2hex(random_bytes(24));$name=$reference.'.'.$types[$mime];$file->move($directory,$name);
            try {$this->cache()->set($this->cacheKey('outplant:upload:'.$reference),['username'=>$account['username'],'kind'=>$kind,'name'=>$name],86400);}
            catch(\Throwable $error){@unlink($directory.$name);throw $error;}
            return $this->result(['reference'=>$reference]);
        });
    }
    private function reference($value,$account,$kind,&$uploads) {
        if (!is_string($value) || !preg_match('/^[a-f0-9]{48}$/',$value) || isset($uploads[$value])) $this->fail('图片引用缺失或重复');
        $upload=$this->cache()->get($this->cacheKey('outplant:upload:'.$value));
        if (!$upload || $upload['username']!==$account['username'] || $upload['kind']!==$kind || !is_file($this->stagingDirectory().$upload['name'])) $this->fail('图片已失效或用途不符，请重新选择');
        $uploads[$value]=$upload;return $value;
    }
    private function attach($upload,&$paths) {
        $path=Filesystem::disk('public')->putFileAs('wx_upload/'.date('Ymd').'/out_plant',new \think\File($this->stagingDirectory().$upload['name']),$upload['name']);
        if (!$path) $this->fail('图片保存失败',503);
        $paths[]=$path;return '/storage/'.str_replace('\\','/',$path);
    }
    private function writeLock($action) {
        // MyISAM保持原引擎；表锁覆盖旧连接写入，创建异常补偿仍不能替代崩溃时的事务回滚。
        $prefix=$this->app->config->get('database.connections.'.$this->app->config->get('database.default').'.prefix');
        if (!preg_match('/^[a-zA-Z0-9_]*$/',$prefix)) throw new \RuntimeException('Invalid prefix');
        Db::query('SELECT 1');$connection=Db::connect()->getConnection();$pdo=$connection->getPdo();
        $pdo->exec('LOCK TABLES `'.$prefix.'common_outplant` WRITE, `'.$prefix.'common_outplant_material` WRITE, `'.$prefix.'admin_user` READ, `'.$prefix.'admin_org` READ, `'.$prefix.'admin_base` READ');
        try {return $action();} finally {$connection->free();$pdo->exec('UNLOCK TABLES');}
    }
    private function chain($admin,$materials,$supervisor) {
        $types=array_column($materials,'type');$chain=['apv1'=>$supervisor,'apv2'=>null,'apv3'=>null];
        $company=in_array(1,$types,true);$scrap=count(array_unique($types))===1 && $types[0]===3;
        if (!$scrap && ((int)$admin['dpt'] <= 0 || !Db::name('admin_user')->where(['id'=>$supervisor,'status'=>1,'dpt'=>$admin['dpt']])->find())) $this->fail('请选择本部门启用的主管审批人');
        if ($company) {
            $department=Db::name('admin_org')->where(['id'=>$admin['dpt'],'mid'=>0])->field('id,title')->find();
            $manager=$department ? OutplantApprovers::managerState($department) : null;
            if (!$manager || !$manager['approverId']) $this->fail('部门经理待指定或代理已失效，请联系管理员在审批人员管理中设置');
            $chain['apv2']=$manager['approverId'];
        }
        if ($scrap) {
            $people=OutplantApprovers::candidates(21);
            if (!$people) $this->fail('废品审批人员未设置或已失效，请联系管理员在审批人员管理中设置');
            $chain['apv1']=$people[0];$chain['apv2']=$people[0];
        }
        if ($company || $scrap) {
            $finance=OutplantApprovers::candidates(20);
            if (!$finance) $this->fail('没有启用的财务审批人，请联系管理员在物品出厂申请页面设置');
            $chain['apv3']=(int)$finance[random_int(0,count($finance)-1)];
        }
        foreach(array_filter($chain) as $id) if (!Db::name('admin_user')->where(['id'=>$id,'status'=>1])->find()) $this->fail('必要审批人已停用或不存在');
        if (!$chain['apv1']) $this->fail('请选择主管审批人');
        return $chain;
    }
    public function create() {
        return $this->guarded(function() {
            $this->requireMethod('POST');$account=$this->current();$this->admin($account);
            // ponytail: 同账号提交串行，受控图片引用仅消费一次，无新增业务表。
            $key='outplant:'.substr(hash('sha256',$this->cacheKey($account['username'])),0,48);
            $lock=Db::query('SELECT GET_LOCK(?,5) acquired',[$key]);if ((int)$lock[0]['acquired']!==1) $this->fail('提交处理中，请稍后重试',409);
            try {
                $submission=$this->text($this->request->post('submissionId'),64,'提交标识');
                if (!preg_match('/^[a-zA-Z0-9-]+$/',$submission)) $this->fail('提交标识不正确');
                $receiptKey=$this->cacheKey('outplant:receipt:'.hash('sha256',$account['username'].':'.$submission));
                $fingerprint=hash('sha256',json_encode($this->request->post()));
                $receipt=$this->cache()->get($receiptKey);
                if ($receipt) {if ($receipt['fingerprint']!==$fingerprint) $this->fail('同一提交标识不能用于不同申请',409);return $this->result(['id'=>(int)$receipt['id']]);}
                $reason=$this->text($this->request->post('reason'),300,'申请原因');$time=$this->text($this->request->post('outTime'),16,'出厂时间');
                $date=\DateTime::createFromFormat('!Y-m-d H:i',$time);if (!$date || $date->format('Y-m-d H:i')!==$time) $this->fail('出厂时间不正确');
                $check=$this->integer($this->request->post('checkType'),'审批类型');$back=$this->integer($this->request->post('backType'),'是否返厂');$express=$this->integer($this->request->post('express'),'是否快递');
                if (!in_array($check,[1,2,3],true) || !in_array($back,[1,2],true) || !in_array($express,[1,2],true)) $this->fail('申请选项不正确');
                $code=$express===2?$this->text($this->request->post('expressCode'),255,'快递单号'):'';
                $raw=$this->request->post('materials');if (!is_array($raw) || !$raw || count($raw)>100) $this->fail('请填写1至100条物料');
                $uploads=[];$materials=[];
                foreach($raw as $item) {
                    if (!is_array($item)) $this->fail('物料不正确');$type=$this->integer($item['type'] ?? null,'物料类别');if (!in_array($type,[1,2,3],true)) $this->fail('物料类别不正确');
                    $num=$this->text($item['num'] ?? null,10,'物料数量');if (!preg_match('/^\d+(\.\d+)?$/',$num) || (float)$num<=0) $this->fail('数量必须是正数，可填写小数');
                    $materials[]=['type'=>$type,'name'=>$this->text($item['name'] ?? null,50,'物料名称'),'unit'=>$this->text($item['unit'] ?? null,10,'物料单位'),'num'=>$num,'mark'=>$this->text($item['mark'] ?? '',255,'物料备注',true),'reference'=>$this->reference($item['reference'] ?? null,$account,'material',$uploads)];
                }
                $approval=$check>1?$this->reference($this->request->post('approvalReference'),$account,'approval',$uploads):null;
                $scrap=count(array_unique(array_column($materials,'type')))===1 && $materials[0]['type']===3;
                $supervisor=$scrap?0:$this->integer($this->request->post('supervisorId'),'主管审批人');$paths=[];
                try {
                    $row=$this->writeLock(function() use($account,$materials,$uploads,$approval,$supervisor,$reason,$date,$check,$back,$express,$code,&$paths) {
                        $admin=$this->admin($account);$chain=$this->chain($admin,$materials,$supervisor);
                        $row=array_merge($chain,['type'=>2,'owner'=>(int)$account['admin_id'],'ownerType'=>(int)$account['user_type']>1?(int)$account['user_type']:2,'dpt'=>$admin['dpt'],'reason'=>$reason,'create_time'=>time(),'out_time'=>$date->getTimestamp(),'checkType'=>$check,'backType'=>$back,'return_status'=>$back===2?1:3,'express'=>$express,'expressCode'=>$code,'img'=>$approval?$this->attach($uploads[$approval],$paths):'','status'=>1,'on_check'=>$chain['apv1']]);
                        $id=Db::name('common_outplant')->insertGetId($row);
                        try {
                            foreach($materials as $material) {$material['img']=$this->attach($uploads[$material['reference']],$paths);unset($material['reference']);$material['n_id']=$id;Db::name('common_outplant_material')->insert($material);}
                        } catch(\Throwable $error) {Db::name('common_outplant_material')->where(['n_id'=>$id,'item'=>1])->delete();Db::name('common_outplant')->where('id',$id)->delete();throw $error;}
                        $row['id']=$id;return $row;
                    });
                } catch(\Throwable $error) {foreach($paths as $path) Filesystem::disk('public')->delete($path);throw $error;}
                try {$this->cache()->set($receiptKey,['id'=>$row['id'],'fingerprint'=>$fingerprint],86400);}catch(\Throwable $error){Log::warning('oms outplant submission receipt unavailable');}
                foreach($uploads as $reference=>$upload) {@unlink($this->stagingDirectory().$upload['name']);try {$this->cache()->delete($this->cacheKey('outplant:upload:'.$reference));} catch(\Throwable $error){Log::warning('oms outplant upload cleanup failed');}}
            } finally {Db::query('SELECT RELEASE_LOCK(?)',[$key]);}
            $this->notify($row,false);return $this->result(['id'=>(int)$row['id']]);
        });
    }
    public function approve() {
        return $this->guarded(function() {
            $this->requireMethod('POST');$account=$this->current();$this->admin($account);$decision=$this->request->post('decision');
            if (!in_array($decision,['pass','reject'],true)) $this->fail('审批操作不正确');
            $reason=$decision==='reject'?$this->text($this->request->post('reason'),255,'驳回原因'):null;
            $row=$this->writeLock(function() use($account,$decision,$reason) {
                $this->admin($account);$row=$this->row($account,$this->request->post('id'));
                if (!$this->permissions($row,$account)['canApprove']) $this->fail('申请状态已变化或不是当前审批人，请刷新',409);
                $stage=(int)$row['status'];$changes=['apv'.$stage.'_time'=>time()];
                if ($decision==='reject') $changes+=['apv'.$stage.'_mark'=>$reason,'status'=>0,'on_check'=>null];
                else {
                    if ($stage===1 && (int)$row['apv1']===(int)$row['apv2']) {$changes['apv2_time']=time();$stage=2;}
                    $next=4;for($i=$stage+1;$i<=3;$i++) if ((int)$row['apv'.$i]>0) {$next=$i;break;}
                    $changes+=['status'=>$next,'on_check'=>$next<4?(int)$row['apv'.$next]:null];
                    if ($next<4 && !Db::name('admin_user')->where(['id'=>$changes['on_check'],'status'=>1])->find()) $this->fail('下一级审批人已停用，请联系管理员',409);
                }
                Db::name('common_outplant')->where('id',$row['id'])->update($changes);return array_merge($row,$changes);
            });
            $this->notify($row,empty($row['on_check']));return $this->result();
        });
    }
    public function remove() {
        return $this->guarded(function() {
            $this->requireMethod('POST');$account=$this->current();$this->admin($account);
            $this->writeLock(function() use($account){$this->admin($account);$row=$this->row($account,$this->request->post('id'));if (!$this->permissions($row,$account)['canDelete']) $this->fail('只能删除本人尚未审批的申请',409);Db::name('common_outplant')->where('id',$row['id'])->update(['status'=>-1,'on_check'=>null]);});
            return $this->result();
        });
    }
    protected function notify($row,$finished) {
        try {
            $service=$this->templateService();$page='pages/outplant/detail?id='.$row['id'];
            if ($finished) {
                $stage=0;for ($i=1;$i<=3;$i++) if (!empty($row['apv'.$i.'_time'])) $stage=$i;
                return $service->send((int)$row['status']===0?'workflow_rejected':'workflow_approved',$service->admins([$row['owner']]),[
                    'name'=>'物品外出审批','approver'=>$service->adminName($row['apv'.$stage] ?? 0),'number'=>(string)$row['id'],'time'=>$service::time(time()),
                ],$page);
            }
            return $service->send('workflow_pending',$service->admins([$row['on_check']]),[
                'name'=>'物品外出审批','initiator'=>$service->adminName($row['owner']),'time'=>$service::time($row['create_time']),
                'reason'=>$row['reason'],'step'=>'第'.$row['status'].'级审批',
            ],$page);
        } catch (\Throwable $error) { Log::warning('oms outplant notification unavailable: '.$error->getMessage()); return false; }
    }
}
