<?php
declare(strict_types=1);
final class RopeSqlGate {
 public static $runtime='';public static $scenario='';public static $case=[];public static $callContext=false;public static $started=0;public static $writes=0;
 public static $tables=['rope_skipping','store','user','staff','store_setting','user_oauth','upload_file'];
 public static function record($r):void{$r['scenario']=self::$scenario;$j=json_encode($r,JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES|JSON_PRESERVE_ZERO_FRACTION);if($j===false)throw new RuntimeException('Native evidence encoding');file_put_contents(self::$runtime.'/pdo-native.jsonl',$j."\n",FILE_APPEND|LOCK_EX);chmod(self::$runtime.'/pdo-native.jsonl',0600);}
 public static function kind(string $sql):string{
  $plain=preg_replace("/'(?:''|\\\\.|[^'\\\\])*'/s",'?', $sql);if(preg_match('/;|--|\/\*|\*\/|#|\b(?:UPDATE|DELETE|REPLACE|MERGE|CREATE|ALTER|DROP|TRUNCATE|GRANT|REVOKE|OUTFILE|DUMPFILE|LOAD|DO|EXEC|EXECUTE|SLEEP|BENCHMARK|UTL_HTTP)\b/i',$plain))throw new RuntimeException('Hard forbidden SQL');
  if(preg_match('/^\s*SELECT\b/i',$plain)){if(preg_match('/\bINTO\b/i',$plain))throw new RuntimeException('SELECT INTO forbidden');if(preg_match_all('/\b(?:yoshop|ydy)_([a-z_0-9]+)/i',$plain,$m))foreach($m[1] as $t)if(!in_array(strtolower($t),self::$tables,true))throw new RuntimeException('Unlisted business table');return 'SELECT';}
  if(preg_match('/^SHOW FULL COLUMNS FROM `(yoshop|ydy)_([a-z_]+)`$/D',$sql,$m)&&in_array($m[2],self::$tables,true))return 'metadata_SHOW';
  if($sql==='CALL SP_TABLEDEF(?, ?)'&&self::$callContext)return 'metadata_CALL';
  if(preg_match('/^\s*INSERT\s+INTO\s+[`"]?yoshop_rope_skipping[`"]?\s*(?:\(|SET\b)/i',$sql)&&strpos(self::$scenario,'ADD_')===0&&isset(self::$case['form']))return 'INSERT';
  throw new RuntimeException('SQL outside exact frozen scope');
 }
 public static function metadata($sql,$bind):void{self::$callContext=false;if($sql!=='CALL SP_TABLEDEF(?, ?)')return;$b=[];foreach($bind as $v){if(is_array($v)){if(count($v)!==2||$v[1]!==PDO::PARAM_STR)throw new RuntimeException('Metadata STR only');$v=$v[0];}$b[]=$v;}if(count($b)!==2||!is_string($b[0])||!is_string($b[1]))throw new RuntimeException('Metadata exact strings');if(!($b[0]==='AIZHCT_APP'&&strpos($b[1],'YOSHOP_')===0&&in_array(strtolower(substr($b[1],7)),self::$tables,true))&&!($b[0]==='ZHCT_APP'&&$b[1]==='YDY_USER'))throw new RuntimeException('Metadata exact schema/table');self::$callContext=true;}
 public static function writePlan($sql,$bind):void{
  if(self::kind($sql)!=='INSERT')return;if(self::$writes!==0)throw new RuntimeException('Single formal insert budget');
  if(preg_match('/^\s*INSERT\s+INTO\s+[`"]?yoshop_rope_skipping[`"]?\s*\(([^)]+)\)\s*VALUES\s*\(([^)]+)\)\s*$/i',$sql,$m)){$columns=array_map(static function($s){return strtolower(trim($s," \t\r\n`\""));},explode(',',$m[1]));$tokens=array_map('trim',explode(',',$m[2]));}
  elseif(getenv('ROPE_LANE')==='B'&&preg_match('/^\s*INSERT\s+INTO\s+`yoshop_rope_skipping`\s+SET\s+(.+?)\s*$/is',$sql,$m)){$columns=[];$tokens=[];foreach(explode(',',$m[1]) as $pair){if(!preg_match('/^\s*`([a-z_]+)`\s*=\s*(:[A-Za-z0-9_]+)\s*$/D',$pair,$match))throw new RuntimeException('Exact MySQL SET named assignment');$columns[]=$match[1];$tokens[]=$match[2];}}
  else throw new RuntimeException('Only exact single row INSERT');
  $form=self::$case['form'];$u=getenv('ROPE_LANE')==='B'?46914:1928;$expected=['user_id'=>(string)$u,'store_id'=>'2147001111','mac'=>$form['mac'],'mode'=>'free','mode_label'=>'CDX精确跳绳','stopped_at'=>$form['stoppedAt'],'date_key'=>$form['dateKey'],'date_label'=>'2097 marker','time_label'=>'12:34:56','count'=>$form['count'],'duration'=>$form['duration'],'calories'=>'12.25','fat_burn_efficiency'=>'0.125','avg_freq'=>'100','max_freq'=>'120','most_jump'=>'90','rope_trips'=>'2','target_seconds'=>'60','data'=>json_encode($form['data'],JSON_UNESCAPED_UNICODE),'create_time'=>null,'update_time'=>null];
  if(count($columns)!==count($expected)||count($tokens)!==count($columns)||count(array_unique($columns))!==count($columns))throw new RuntimeException('INSERT exact columns');
  foreach($columns as $i=>$column){if(!array_key_exists($column,$expected))throw new RuntimeException('Unapproved write column');$k=ltrim($tokens[$i],':');if(!array_key_exists($k,$bind)&&!array_key_exists(':'.$k,$bind))throw new RuntimeException('Named bind required');$v=$bind[$k]??$bind[':'.$k];if(is_array($v))$v=$v[0];if($expected[$column]===null){if(!is_int($v)||$v<self::$started-2||$v>time()+2)throw new RuntimeException('Original time bound');}elseif((string)$v!==$expected[$column])throw new RuntimeException('Exact write value mismatch '.$column);}
 }
}
class RopeStatement extends PDOStatement {
 private $bound=[];protected function __construct(){}
 public function bindValue($parameter,$value,$type=PDO::PARAM_STR){$ok=parent::bindValue($parameter,$value,$type);$this->bound[(string)$parameter]=['value'=>$value,'pdo_type'=>$type,'php_type'=>gettype($value),'ok'=>$ok];return $ok;}
 public function bindParam($parameter,&$value,$type=PDO::PARAM_STR,$length=0,$options=null){throw new RuntimeException('bindParam not approved');}
 public function execute($params=null){$kind=RopeSqlGate::kind($this->queryString);$r=['layer'=>'actual PDOStatement.execute','queryString'=>$this->queryString,'bindValue'=>$this->bound,'execute_array'=>$params,'operation'=>$kind];try{$ok=parent::execute($params);$r['success']=$ok;$r['rowCount']=$this->rowCount();if($kind==='INSERT'){RopeSqlGate::$writes++;if($r['rowCount']!==1)throw new RuntimeException('Actual INSERT affected budget');}RopeSqlGate::record($r);return $ok;}catch(Throwable $e){$r['error']=['class'=>get_class($e),'code'=>$e->getCode(),'message'=>$e->getMessage()];RopeSqlGate::record($r);throw $e;}}
}
class RopePDO extends PDO {
 public function prepare($sql,$options=[]){$kind=RopeSqlGate::kind($sql);try{$s=parent::prepare($sql,$options);RopeSqlGate::record(['layer'=>'actual PDO.prepare','queryString'=>$sql,'operation'=>$kind,'prepared'=>$s!==false]);return $s;}catch(Throwable $e){RopeSqlGate::record(['layer'=>'actual PDO.prepare','queryString'=>$sql,'operation'=>$kind,'error'=>['class'=>get_class($e),'code'=>$e->getCode(),'message'=>$e->getMessage()]]);throw $e;}}
 public function exec($sql){throw new RuntimeException('Direct exec forbidden');}
 public function query($sql,...$args){RopeSqlGate::kind($sql);$s=$this->prepare($sql);$s->execute();if($args)$s->setFetchMode(...$args);return $s;}
}
trait RopeConnection {
 protected function createPdo($dsn,$username,$password,$params){$dm=getenv('POC_DIALECT')==='dm';$key=null;foreach(['AI','ZHCT'] as $k){$expected=$dm?'dm:host='.getenv('POC_'.$k.'_HOST').';port='.getenv('POC_'.$k.'_PORT').';schema='.getenv('POC_'.$k.'_SCHEMA'):'mysql:host='.getenv('POC_'.$k.'_HOST').';port='.getenv('POC_'.$k.'_PORT').';dbname='.getenv('POC_'.$k.'_SCHEMA').';charset='.getenv('POC_'.$k.'_CHARSET');if($dsn===$expected&&$username===getenv('POC_'.$k.'_USER')&&$password===getenv('POC_'.$k.'_PASSWORD'))$key=$k;}if($key===null||isset($params[1002]))throw new RuntimeException('Exact two development connections only');$p=new RopePDO($dsn,$username,$password,$params);if(!$p->setAttribute(PDO::ATTR_STATEMENT_CLASS,[RopeStatement::class,[]]))throw new RuntimeException('Original native PDO statement capture unavailable');return $p;}
 public function getPDOStatement(string $sql,array $bind=[],bool $master=false,bool $procedure=false):PDOStatement{RopeSqlGate::metadata($sql,$bind);try{RopeSqlGate::kind($sql);RopeSqlGate::writePlan($sql,$bind);RopeSqlGate::record(['layer'=>'framework original bind plan','queryString'=>$sql,'bind_plan'=>$bind,'procedure'=>$procedure]);return parent::getPDOStatement($sql,$bind,$master,$procedure);}catch(Throwable $e){RopeSqlGate::record(['layer'=>'framework intercepted failure; may precede PDO.prepare','queryString'=>$sql,'bind_plan'=>$bind,'error'=>['class'=>get_class($e),'code'=>$e->getCode(),'message'=>$e->getMessage()]]);throw $e;}finally{RopeSqlGate::$callContext=false;}}
}
class RopeGuardMysql extends \think\db\connector\Mysql {use RopeConnection;public function getBuilderClass():string{return \think\db\builder\Mysql::class;}}
class RopeGuardDm extends \app\common\database\Dm {use RopeConnection;}
