<?php
namespace app\mobile\service;

use think\facade\Db;
use think\facade\Cache;

/** 待办由业务权限实时核对；消息表只保存收件、已读及处理历史。 */
class MessageInbox {
    const SOURCES = ['visitor'=>'OmsVisitor','outplant'=>'OmsOutplant','material-return'=>'OmsMaterialReturn',
        'suggestion'=>'OmsSuggestion','safety-risk'=>'OmsSafetyRisk','performance'=>'OmsPerformance',
        'leave'=>'OmsLeaveCancel','daily-issue'=>'OmsDailyIssue','key-exam'=>'OmsKeyPositionExam','night-bus'=>'OmsNightBus','check-issue'=>'OmsCheckIssue'];
    const LABELS = ['visitor'=>'访客','outplant'=>'出厂','material-return'=>'返厂','suggestion'=>'建议','safety-risk'=>'安全风险',
        'performance'=>'绩效','leave'=>'离职','daily-issue'=>'Daily Issue','key-exam'=>'考试','night-bus'=>'班车','check-issue'=>'点检异常'];
    private $app;
    private $activeSource;
    public function __construct($app) { $this->app = $app; }
    protected function source($name) { $class = '\\app\\mobile\\controller\\'.$name; return new $class($this->app); }
    public function allowsSource($source) { return $this->activeSource === $source; }
    private function collect($name,$account,$since) {
        $source=$this->source($name);$this->activeSource=$source;
        try { return $source->messageItems($account,$since,$this); }
        finally { $this->activeSource=null; }
    }
    public static function item($business, $id, $node, $title, $stage, $fields, $route, $state='pending', $time=0, $deadline=0) {
        $clean = [];
        foreach ($fields as $label=>$value) if ((string)$value !== '') $clean[] = ['label'=>$label,'value'=>mb_substr(html_entity_decode(strip_tags((string)$value),ENT_QUOTES,'UTF-8'),0,300)];
        return ['business'=>$business,'business_id'=>(int)$id,'node'=>$node,'state'=>$state,'title'=>$title,'stage'=>$stage,
            'fields'=>json_encode($clean,JSON_UNESCAPED_UNICODE),'route'=>$route,'task_at'=>max(0,(int)$time),'deadline'=>max(0,(int)$deadline)];
    }
    private static function cachePrefix() {
        $name=config('database.default');
        return 'oms-inbox-v2:'.sha1((string)config('database.connections.'.$name.'.database')).':';
    }
    public static function invalidate() {
        Cache::store('redis')->set(self::cachePrefix().'revision',bin2hex(random_bytes(8)),0);
    }
    public function summary($account) {
        $cache=Cache::store('redis');$revision=$cache->get(self::cachePrefix().'revision','');
        $key=self::cachePrefix().'counts:'.$account['id'];$now=time();
        $identity=hash('sha256',json_encode([$account['version']??0,$account['user_id'],$account['admin_id'],$account['user_type']]));
        $saved=$cache->get($key);
        if($saved && $saved['revision']===$revision && $saved['identity']===$identity && $saved['until']>$now)return $saved['counts'];
        $counts=$this->sync($account);
        // 班车、考试及跨日边界不能沿用上一时间窗的计数。
        $until=$now+15;
        foreach([strtotime('today 21:45'),strtotime('today 22:45')+1,strtotime('tomorrow')] as $at)if($at>$now)$until=min($until,$at);
        foreach(Db::name('message')->where('auth_id',$account['id'])->whereIn('state',['pending','notice'])->field('task_at,deadline')->select()->toArray() as $row){
            foreach([(int)$row['task_at'],(int)$row['deadline'],(int)$row['deadline']+1] as $at)if($at>$now)$until=min($until,$at);
        }
        $cache->set($key,['counts'=>$counts,'revision'=>$revision,'identity'=>$identity,'until'=>$until],max(1,$until-time()));
        return $counts;
    }
    private static function changes($old,$next) {
        return array_filter($next,function($value,$key)use($old){return !array_key_exists($key,$old)||(string)$old[$key] !== (string)$value;},ARRAY_FILTER_USE_BOTH);
    }
    public function sync($account,$businesses=null) {
        $sources=$businesses===null?self::SOURCES:array_intersect_key(self::SOURCES,array_flip($businesses));
        if(!$sources)throw new \InvalidArgumentException('Invalid inbox sources');
        $database = $this->app->config->get('database.connections.'.$this->app->config->get('database.default').'.database');
        $lock = 'oms-inbox:'.sha1($database.':'.$account['id']);
        $result = Db::query('SELECT GET_LOCK(?,5) acquired',[$lock]);
        if ((int)$result[0]['acquired'] !== 1) throw new \RuntimeException('Message synchronization busy');
        try {
            // 全部来源成功后才关闭旧待办；接口故障不会把待办误清空。
            $items = []; $since = time()-30*86400;
            if((int)$account['admin_id']>0 && !\app\mobile\controller\OmsVisitor::adminFor($account))$account['admin_id']=0;
            foreach ($sources as $business=>$name) {
                foreach ($this->collect($name,$account,$since) as $item) $items[]=$item;
            }
            return Db::transaction(function() use($account,$items,$sources) {
                $existingMap=[];$ids=array_unique(array_column($items,'business_id'));
                if($ids)foreach(Db::name('message')->where('auth_id',$account['id'])->whereIn('business',array_keys($sources))->whereIn('business_id',$ids)->select()->toArray() as $row)$existingMap[$row['business'].':'.$row['business_id'].':'.$row['node']]=$row;
                $active = []; $now = time();
                foreach ($items as $item) {
                    $key = ['auth_id'=>(int)$account['id'],'business'=>$item['business'],'business_id'=>$item['business_id'],'node'=>$item['node']];
                    $existing = $existingMap[$item['business'].':'.$item['business_id'].':'.$item['node']] ?? null;
                    $changes = $item + ['updated_at'=>$now];
                    if($existing && $item['state']==='pending' && $existing['state']==='done'){
                        Db::name('message')->where('id',$existing['id'])->update(['node'=>mb_substr($existing['node'],0,40).'-history-'.$existing['id']]);
                        $existing=null;
                    }
                    if ($existing) {
                        if($item['state']==='notice' && ($existing['title']!==$item['title'] || $existing['stage']!==$item['stage']))$changes['read_at']=0;
                        if ($item['state']==='pending' && $existing['state']!=='pending') $changes+=['read_at'=>0,'handled_at'=>0,'created_at'=>$now];
                        if ($item['state']==='done' && !$existing['handled_at']) $changes['handled_at']=$item['task_at'] ?: $now;
                        unset($changes['updated_at']);$changes=self::changes($existing,$changes);
                        if($changes)Db::name('message')->where('id',$existing['id'])->update($changes+['updated_at'=>$now]); $id=(int)$existing['id'];
                    } else {
                        $id=(int)Db::name('message')->insertGetId($changes+$key+['created_at'=>$now,'handled_at'=>$item['state']==='done'?($item['task_at'] ?: $now):0]);
                    }
                    if ($item['state']==='pending') $active[]=$id;
                }
                $old=Db::name('message')->where(['auth_id'=>$account['id'],'state'=>'pending'])->whereIn('business',array_keys($sources));
                if ($active) $old->whereNotIn('id',$active);
                $old->update(['state'=>'closed','updated_at'=>$now]);
                if(isset($sources['key-exam'])){
                $scheduled=Db::name('message')->where(['auth_id'=>$account['id'],'business'=>'key-exam','node'=>'exam','state'=>'notice']);
                $futureIds=[];foreach($items as $item)if($item['business']==='key-exam' && $item['state']==='notice')$futureIds[]=$item['business_id'];
                if($futureIds)$scheduled->whereNotIn('business_id',$futureIds);
                $scheduled->update(['state'=>'closed','updated_at'=>$now]);
                }
                return $this->counts($account['id']);
            });
        } finally { Db::query('SELECT RELEASE_LOCK(?)',[$lock]); }
    }
    public function counts($authId) {
        $rows=Db::name('message')->where('auth_id',$authId)->field('state,COUNT(*) total')->group('state')->select()->toArray();
        $counts=['pending'=>0,'done'=>0,'notice'=>0];
        foreach($rows as $row) if(isset($counts[$row['state']])) $counts[$row['state']]=(int)$row['total'];
        $counts['unread']=Db::name('message')->where(['auth_id'=>$authId,'state'=>'notice','read_at'=>0])->count();
        return $counts;
    }
    public function pendingFor($authId,$business,$id) {
        return Db::name('message')->where(['auth_id'=>$authId,'business'=>$business,'business_id'=>$id,'state'=>'pending'])->column('id');
    }
    public function finish($authId,$ids,$action='') {
        if (!$ids) return;
        // 业务仍允许继续处理（例如保存整改草稿、考试未通过）时保留待办。
        foreach(Db::name('message')->where('auth_id',$authId)->whereIn('id',$ids)->where('state','closed')->select()->toArray() as $row){
            $stage=$action==='transfer'?'已转交':'已处理';
            if($row['business']==='suggestion' && $action==='transfer'){
                $stage=Db::name('onsite_suggestion_list')->where('id',$row['business_id'])->value('transfer_id')?'转移待确认':'已分配';
            }
            $operation=['assign'=>'分配','feedback'=>'反馈','confirm'=>'确认','publish'=>'发布','communicate'=>'沟通','verify'=>'核验','exam'=>'考试'][$row['node']] ?? '审批';
            if($row['business']==='suggestion' && strpos($row['node'],'transfer-')===0)$operation='转移接收';
            $title=self::LABELS[$row['business']].($action==='transfer'?'责任':$operation).$stage;
            if($row['business']==='suggestion' && $action==='transfer')$title=$stage==='已分配'?'建议负责人已分配':'建议转移申请已发送';
            Db::name('message')->where(['id'=>$row['id'],'auth_id'=>$authId,'state'=>'closed'])->update(['state'=>'done','title'=>$title,'stage'=>$stage,'handled_at'=>time(),'read_at'=>time(),'updated_at'=>time()]);
        }
    }
    public static function business($controller) {
        foreach(self::SOURCES as $business=>$name)if(is_a($controller,'\\app\\mobile\\controller\\'.$name,true))return $business;
        return '';
    }
    /** 业务写入后保存结果通知，长期申请也不会受历史回填时间范围影响。 */
    public function captureResult($business,$id) {
        $id=(int)$id;
        $tables=['visitor'=>'common_visitor_list','outplant'=>'common_outplant','material-return'=>'common_outplant_back',
            'suggestion'=>'onsite_suggestion_list','safety-risk'=>'onsite_safe_list','performance'=>'kpi_record','leave'=>'member_perleave','daily-issue'=>'quality_wir_list','check-issue'=>'me_check_issue'];
        if(!$id || !isset($tables[$business]))return;
        $row=Db::name($tables[$business])->find($id);if(!$row)return;
        $admins=[];$employees=[];
        if($business==='visitor' && in_array((int)$row['status'],[-1,0,2],true))$admins[]=(int)$row['owner'];
        elseif($business==='outplant' && (int)$row['ownerType']>1 && in_array((int)$row['status'],[0,4,5],true))$admins[]=(int)$row['owner'];
        elseif($business==='material-return' && (int)$row['schema_version']===1 && in_array((int)$row['status'],[0,2],true))$admins[]=(int)$row['owner'];
        elseif(in_array($business,['suggestion','safety-risk'],true) && (int)$row['status']===4){$admins[]=(int)$row['owner'];$employees[]=(string)$row['eid'];}
        elseif($business==='performance' && in_array((int)$row['status'],[2,3,4],true)){$admins[]=(int)$row['aid'];$employees[]=(string)$row['eid'];}
        elseif($business==='leave' && in_array((int)$row['status'],[2,3],true)){$eid=Db::name('member_list')->where('id',$row['mid'])->value('eid');if($eid)$employees[]=(string)$eid;}
        elseif($business==='daily-issue' && (int)$row['status']===2)$admins[]=(int)$row['owner'];
        elseif($business==='check-issue'){ $admins[]=(int)$row['owner'];$admins[]=(int)Db::name('team_list')->where('id',$row['tid'])->value('aid');$eid=Db::name('member_list')->where('id',$row['mid'])->value('eid');if($eid)$employees[]=(string)$eid; }
        $admins=array_filter($admins);if(!$admins && !$employees)return;
        $accounts=Db::name('auth')->where('status',1)->where(function($q)use($admins,$employees){$q->where('id',-1);if($admins)$q->whereOr('admin_id','in',$admins);if($employees)$q->whereOr('username','in',$employees);})->select()->toArray();
        foreach($accounts as $account){
            $table=(int)$account['user_type']===1?'member_list':'admin_user';$key=(int)$account['user_type']===1?'user_id':'admin_id';$field=(int)$account['user_type']===1?'eid':'username';
            if(!Db::name($table)->where(['id'=>$account[$key],$field=>$account['username'],'status'=>1])->find())continue;
            if((int)$account['admin_id']>0 && !\app\mobile\controller\OmsVisitor::adminFor($account))$account['admin_id']=0;
            foreach($this->collect(self::SOURCES[$business],$account,0) as $item){
                if($item['business_id']!==$id || $item['state']!=='notice')continue;
                $key=['auth_id'=>$account['id'],'business'=>$business,'business_id'=>$id,'node'=>$item['node']];
                // 同一结果重试不会复位已读；与按人同步共用唯一键。
                $database=$this->app->config->get('database.connections.'.$this->app->config->get('database.default').'.database');
                $lock='oms-inbox:'.sha1($database.':'.$account['id']);$acquired=Db::query('SELECT GET_LOCK(?,5) acquired',[$lock]);
                if((int)$acquired[0]['acquired']!==1)throw new \RuntimeException('Message result busy');
                try{$old=Db::name('message')->where($key)->find();if($old){$changes=self::changes($old,$item);if($changes)Db::name('message')->where('id',$old['id'])->update($changes+['updated_at'=>time()]);}else Db::name('message')->insert($item+$key+['created_at'=>time(),'updated_at'=>time()]);}
                finally{Db::query('SELECT RELEASE_LOCK(?)',[$lock]);}
            }
        }
    }
    public static function mutation($controller,$action,$request) {
        $map=['OmsVisitor'=>['visitor',['approve']], 'OmsOutplant'=>['outplant',['approve']],
            'OmsMaterialReturn'=>['material-return',['approve']], 'OmsSuggestion'=>['suggestion',['assign','transfer','respondtransfer','feedback','confirm']],
            'OmsSafetyRisk'=>['safety-risk',['assign','feedback','confirm']], 'OmsPerformance'=>['performance',['confirm']],
            'OmsLeaveCancel'=>['leave',['communicate']], 'OmsDailyIssue'=>['daily-issue',['publish']],
            'OmsKeyPositionExam'=>['key-exam',['submit']], 'OmsNightBus'=>['night-bus',['confirm']], 'OmsCheckIssue'=>['check-issue',['assign','feedback','confirm','reject','cancel']]];
        $business=self::business($controller);$name=self::SOURCES[$business] ?? '';
        if ($request->method()!=='POST' || !isset($map[$name]) || !in_array($action,$map[$name][1],true)) return null;
        return [$map[$name][0],(int)$request->post('id')];
    }
}
