<?php
declare(strict_types=1);
final class ReadSqlGate
{
 public static $runtime='';public static $scenario='PREFLIGHT';public static $callContext=false;
 public static $tables=['user','upload_file','user_grade','user_points_log','user_balance_log','user_feedback','dishes','store_user','store_user_role','store_user_relation','store_role','store_role_menu','store_menu','store_menu_api','store_api'];
 public static function write(array $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 invalid UTF8');file_put_contents(self::$runtime.'/pdo-native.jsonl',$j."\n",FILE_APPEND|LOCK_EX);chmod(self::$runtime.'/pdo-native.jsonl',0600);}
 public static function sql(string $sql):string{
  $plain=preg_replace("/'(?:''|\\\\.|[^'\\\\])*'/s","?",$sql);if($plain===null||preg_match('/;|--|\/\*|\*\/|#|\b(?:INSERT|UPDATE|DELETE|REPLACE|MERGE|CREATE|ALTER|DROP|TRUNCATE|GRANT|REVOKE|INTO|OUTFILE|DUMPFILE|LOAD|DO|EXEC|EXECUTE|SLEEP|BENCHMARK|UTL_HTTP|DBMS_SCHEDULER)\b/i',$plain))throw new RuntimeException('Hard SQL read-only gate');
  if(preg_match('/^\s*SELECT\b/i',$plain)){
   if(preg_match_all('/\byoshop_([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');
   if(preg_match('/\b(?:ZHCT_APP|mysql_zhct|INFORMATION_SCHEMA\.(?!COLUMNS|STATISTICS|KEY_COLUMN_USAGE|TRIGGERS))\b/i',$plain))throw new RuntimeException('Cross-connection SQL');return 'SELECT';
  }
  if(preg_match('/^SHOW FULL COLUMNS FROM `yoshop_([a-z_]+)`$/D',$sql,$m)&&in_array($m[1],self::$tables,true))return 'metadata_SHOW';
  if($sql==='CALL SP_TABLEDEF(?, ?)'&&self::$callContext)return 'metadata_CALL';
  throw new RuntimeException('Only exact approved SELECT/metadata allowed');
 }
 public static function call(string $sql,array $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('CALL exact PARAM_STR');$v=$v[0];}if(!is_string($v))throw new RuntimeException('CALL exact string bind');$b[]=$v;}if(count($b)!==2||$b[0]!=='AIZHCT_APP'||!in_array(strtolower(preg_replace('/^YOSHOP_/','',$b[1])),self::$tables,true)||strpos($b[1],'YOSHOP_')!==0)throw new RuntimeException('CALL exact approved schema/table');self::$callContext=true;
 }
}
class ReadOnlyStatement extends PDOStatement
{
 private $bound=[];
 protected function __construct(){}
 public function bindValue($parameter,$value,$dataType=PDO::PARAM_STR){$ok=parent::bindValue($parameter,$value,$dataType);$this->bound[(string)$parameter]=['value'=>$value,'pdo_type'=>$dataType,'php_type'=>gettype($value),'ok'=>$ok];return $ok;}
 public function bindParam($parameter,&$variable,$dataType=PDO::PARAM_STR,$length=0,$driverOptions=null){throw new RuntimeException('Unexpected bindParam procedure');}
 public function execute($params=null){$sql=$this->queryString;$op=ReadSqlGate::sql($sql);$r=['layer'=>'actual PDOStatement prepare/bindValue/execute','queryString'=>$sql,'operation'=>$op,'bindValue'=>$this->bound,'execute_array'=>$params,'success'=>false];try{$ok=parent::execute($params);$r['success']=$ok;$r['rowCount']=$this->rowCount();ReadSqlGate::write($r);return $ok;}catch(Throwable $e){$r['error']=['class'=>get_class($e),'message'=>$e->getMessage(),'code'=>$e->getCode()];ReadSqlGate::write($r);throw $e;}}
}
class ReadOnlyPDO extends PDO
{
 public function prepare($sql,$options=[]){$op=ReadSqlGate::sql($sql);$r=['layer'=>'actual PDO.prepare','queryString'=>$sql,'operation'=>$op,'prepared'=>false];try{$s=parent::prepare($sql,$options);$r['prepared']=$s!==false;ReadSqlGate::write($r);return $s;}catch(Throwable $e){$r['error']=['class'=>get_class($e),'code'=>$e->getCode(),'message'=>$e->getMessage()];ReadSqlGate::write($r);throw $e;}}
 public function exec($sql){throw new RuntimeException('Unexpected direct PDO.exec forbidden');}
 public function query($sql,...$args){ReadSqlGate::sql($sql);$s=$this->prepare($sql);$s->execute();if($args)$s->setFetchMode(...$args);return $s;}
}
trait GuardConnection
{
 protected function createPdo($dsn,$username,$password,$params){$dm=getenv('POC_DIALECT')==='dm';$expected=$dm?'dm:host=127.0.0.1;port=5238;schema=AIZHCT_APP':'mysql:host='.getenv('POC_AI_HOST').';port='.getenv('POC_AI_PORT').';dbname='.getenv('POC_AI_SCHEMA').';charset='.getenv('POC_AI_CHARSET');if($dsn!==$expected||$username!==getenv('POC_AI_USER')||$password!==getenv('POC_AI_PASSWORD'))throw new RuntimeException('Exact approved connection gate');if(isset($params[1002]))throw new RuntimeException('No PDO init command');$p=new ReadOnlyPDO($dsn,$username,$password,$params);if(!$p->setAttribute(PDO::ATTR_STATEMENT_CLASS,[ReadOnlyStatement::class,[]]))throw new RuntimeException('Native PDO statement capture unavailable');return $p;}
 public function getPDOStatement(string $sql,array $bind=[],bool $master=false,bool $procedure=false):PDOStatement{ReadSqlGate::call($sql,$bind);try{ReadSqlGate::sql($sql);return parent::getPDOStatement($sql,$bind,$master,$procedure);}finally{ReadSqlGate::$callContext=false;}}
}
class GuardMysql extends \think\db\connector\Mysql{use GuardConnection;public function getBuilderClass():string{return \think\db\builder\Mysql::class;}}
class GuardDm extends \app\common\database\Dm{use GuardConnection;}
