<?php
namespace app\adminVue\controller;

class V2InventoryImport
{
    private static function directory() {$dir=app()->getRuntimePath().'v2-inventory/';if(!is_dir($dir)&&!mkdir($dir,0700,true)&&!is_dir($dir))throw new \RuntimeException('无法建立导入目录');return $dir;}
    private static function path($ticket) {if(!is_string($ticket)||!preg_match('/^[a-f0-9]{24}$/D',$ticket))throw new \InvalidArgumentException('导入票据无效');return self::directory().$ticket.'.json';}
    public static function save($meta) {$path=self::path($meta['ticket']);$tmp=$path.'.tmp';if(file_put_contents($tmp,json_encode($meta,JSON_UNESCAPED_UNICODE),LOCK_EX)===false||!rename($tmp,$path))throw new \RuntimeException('保存导入状态失败');}
    public static function read($ticket,$binding) {$m=json_decode(@file_get_contents(self::path($ticket)),true);if(!$m||!hash_equals($m['binding'],$binding)||$m['expires']<time())throw new \InvalidArgumentException('导入已过期或不属于当前会话，请重新上传');return $m;}
    private static function drop($pdo,$tables) {foreach($tables as $t)$pdo->exec('DROP TABLE IF EXISTS '.V2InventoryData::quote($t));}
    private static function cleanup($pdo,$prefix) {
        $db=$pdo->query('SELECT DATABASE()')->fetchColumn();
        foreach(glob(self::directory().'*.json') as $path){$m=json_decode(file_get_contents($path),true);if(!$m||($m['expires']??PHP_INT_MAX)>=time()||($m['prefix']??null)!==$prefix||($m['database']??null)!==$db)continue;
            if(in_array($m['state']??'',['ready','preparing'],true))self::drop($pdo,$m['tables']);unlink($path);
        }
    }
    public static function prepare($path,$owner,$binding) {
        return V2InventoryData::lock(function($pdo,$prefix)use($path,$owner,$binding){
            V2InventoryData::guard();self::cleanup($pdo,$prefix);$live=V2InventoryData::live($prefix);$ticket=bin2hex(random_bytes(12));$stages=[];
            foreach($live as $k=>$t)$stages[$k]=$prefix.'v2inv_'.substr($ticket,0,12).'_'.$k;
            $meta=['ticket'=>$ticket,'binding'=>$binding,'expires'=>time()+3600,'prefix'=>$prefix,'database'=>$pdo->query('SELECT DATABASE()')->fetchColumn(),'tables'=>$stages,'state'=>'preparing'];self::save($meta);
            try {
                $sheets=V2InventoryFile::sheets($path);$strings=V2InventoryFile::strings($path);$errors=[];$counts=[];
                $expected=array_map(function($t){return 'oms_'.$t;},V2InventoryData::TABLES);
                if(count($sheets)!==3||array_diff(array_values($expected),array_keys($sheets)))throw new \InvalidArgumentException('文件必须恰好包含oms_onsite_inventory、oms_onsite_inventory_base、oms_onsite_inventory_super三张工作表');
                foreach($live as $kind=>$table){
                    $pdo->exec('CREATE TABLE '.V2InventoryData::quote($stages[$kind]).' LIKE '.V2InventoryData::quote($table));$headers=null;$batch=[];$count=0;$seen=[];
                    $flush=function()use(&$batch,$pdo,$stages,$kind){if(!$batch)return;$cols=array_keys($batch[0]);$sql='INSERT INTO '.V2InventoryData::quote($stages[$kind]).' ('.implode(',',array_map([V2InventoryData::class,'quote'],$cols)).') VALUES '.implode(',',array_fill(0,count($batch),'('.implode(',',array_fill(0,count($cols),'?')).')'));$values=[];foreach($batch as $row)foreach($row as $v)$values[]=$v;$pdo->prepare($sql)->execute($values);$batch=[];};
                    foreach(V2InventoryFile::rows($path,$sheets[$expected[$kind]],$strings) as $line=>$values){
                        if($headers===null){$headers=[];foreach($values as $i=>$v)$headers[$i]=trim($v);if(count(array_unique($headers))!==count($headers)||array_diff(V2InventoryData::FIELDS[$kind],$headers)||array_diff($headers,array_merge(V2InventoryData::FIELDS[$kind],['owner','update'])))throw new \InvalidArgumentException($expected[$kind].'表头不匹配，须使用内网原字段导出');continue;}
                        $count++;if($count>($kind==='base'?300000:100000))throw new \InvalidArgumentException($expected[$kind].'数据超过行数限制');
                        $input=[];foreach($headers as $i=>$h)$input[$h]=$values[$i]??'';
                        try{$row=V2InventoryData::normalize($kind,$input,$owner);if(isset($seen[$row['id']]))throw new \InvalidArgumentException('ID重复');$seen[$row['id']]=true;$batch[]=$row;if(count($batch)>=500)$flush();}
                        catch(\InvalidArgumentException $e){if(count($errors)<100)$errors[]=$expected[$kind].' 第'.$line.'行：'.$e->getMessage();}
                    }
                    if($headers===null)throw new \InvalidArgumentException($expected[$kind].'缺少表头');$flush();$counts[$kind]=$count;
                }
                if(!$counts['address']||!$counts['base'])$errors[]='盘点地址和物料基础清单不能为空';
                V2InventoryData::indexes($pdo,$stages);
                if(!$errors)$errors=V2InventoryData::validate($pdo,$stages,$prefix);
                if($errors){self::drop($pdo,$stages);@unlink(self::path($ticket));return ['valid'=>false,'counts'=>$counts,'errors'=>array_slice($errors,0,100)];}
                $meta['state']='ready';$meta['fingerprint']=V2InventoryData::fingerprint($pdo,$live);$meta['targetFingerprint']=V2InventoryData::fingerprint($pdo,$stages);$meta['counts']=$counts;$meta['oldCounts']=V2InventoryData::counts($pdo,$live);
                $meta['projects']=$pdo->query('SELECT project,COUNT(*) as addresses FROM '.V2InventoryData::quote($stages['address']).' GROUP BY project ORDER BY project')->fetchAll(\PDO::FETCH_ASSOC);
                $warnings=V2InventoryData::validate($pdo,$stages,$prefix,true);
                self::save($meta);return ['valid'=>true,'ticket'=>$ticket,'counts'=>$counts,'oldCounts'=>$meta['oldCounts'],'projects'=>$meta['projects'],'warnings'=>$warnings,'errors'=>[]];
            } catch(\Throwable $e){self::drop($pdo,$stages);@unlink(self::path($ticket));throw $e;}
        });
    }
    public static function commit($ticket,$binding) {
        return V2InventoryData::lock(function($pdo,$prefix)use($ticket,$binding){
            $meta=self::read($ticket,$binding);if($meta['state']==='done')return ['saved'=>true,'counts'=>$meta['counts']];
            $live=V2InventoryData::live($prefix);
            if(($meta['database']??null)!==$pdo->query('SELECT DATABASE()')->fetchColumn())throw new \InvalidArgumentException('导入数据库不匹配');
            if($meta['state']==='switching'){
                $remaining=0;foreach($meta['tables'] as $t){$q=$pdo->prepare('SELECT COUNT(*) FROM information_schema.tables WHERE table_schema=DATABASE() AND table_name=?');$q->execute([$t]);$remaining+=(int)$q->fetchColumn();}
                if($remaining===0&&hash_equals($meta['targetFingerprint'],V2InventoryData::fingerprint($pdo,$live))){$meta['state']='done';$meta['expires']=time()+86400;self::save($meta);return ['saved'=>true,'counts'=>$meta['counts']];}
                if($remaining===3)$meta['state']='ready';
            }
            if($meta['state']!=='ready')throw new \InvalidArgumentException('切换结果需要核对，请勿再次导入');
            V2InventoryData::guard();
            if(!hash_equals($meta['fingerprint'],V2InventoryData::fingerprint($pdo,$live)))throw new \InvalidArgumentException('正式数据已被修改，请重新上传检查，避免覆盖其他人的修改');
            $meta['state']='switching';self::save($meta);
            V2InventoryData::guard();self::swap($pdo,$prefix,$meta['tables']);$meta['state']='done';$meta['expires']=time()+86400;self::save($meta);return ['saved'=>true,'counts'=>$meta['counts']];
        });
    }
    public static function swap($pdo,$prefix,$stages) {
        $parts=[];foreach(V2InventoryData::live($prefix) as $k=>$t){$backup=$prefix.'v2inv_previous_'.$k;$pdo->exec('DROP TABLE IF EXISTS '.V2InventoryData::quote($backup));$parts[]=V2InventoryData::quote($t).' TO '.V2InventoryData::quote($backup);$parts[]=V2InventoryData::quote($stages[$k]).' TO '.V2InventoryData::quote($t);}
        $pdo->exec('RENAME TABLE '.implode(', ',$parts));
    }
    public static function discard($ticket,$binding) {return V2InventoryData::lock(function($pdo)use($ticket,$binding){$m=self::read($ticket,$binding);if($m['state']==='ready')self::drop($pdo,$m['tables']);@unlink(self::path($ticket));return ['discarded'=>true];});}
}
