<?php
declare(strict_types=1);
class NRGate {
    public static $journal=[],$deny=false,$case=null,$started=0,$priorGuids=[],$attempted=false,$inserted=[];
    public static $tables=['ydy_staff','ydy_dishes','ydy_menu_recommend','ydy_date_menu','ydy_config','ydy_identity_type','ydy_user','ydy_dish','ydy_foods_composition','ydy_initial_menu_log','ydy_member_weight_status','ydy_rpe','ydy_rpe_info','ydy_sport','ydy_menu_recommend_record','ydy_menu_recommend_set'];
    public static function sql(string $sql):string {
        if(preg_match('/^INSERT\\b/i',$sql)){self::logRows($sql);return 'approved-log-insert';}
        if(self::$deny)throw new RuntimeException('Alias requires zero native SQL');
        if(preg_match('/^CALL SP_TABLEDEF\(\?, \?\)$/D',$sql))return 'metadata-definition';
        if(preg_match('/^SHOW (?:FULL )?COLUMNS FROM `?(ydy_[a-z_]+)`?$/iD',$sql,$m)&&in_array(strtolower($m[1]),self::$tables,true))return 'metadata-show';
        if(!preg_match('/^\s*SELECT\b/i',$sql))throw new RuntimeException('SELECT-only gate');
        $plain=preg_replace("/'(?:''|[^'])*'/s", "''",$sql);
        if(preg_match('/;|--|\/\*|\b(INSERT|UPDATE|DELETE|MERGE|DROP|ALTER|CREATE|TRUNCATE|CALL|EXEC|INTO|OUTFILE|SLEEP|BENCHMARK|DBMS_LOCK|DBLINK)\b/i',$plain))throw new RuntimeException('Unsafe SELECT');
        preg_match_all('/\b(?:FROM|JOIN)\s+([`"]?[A-Za-z_][A-Za-z_0-9]*[`"]?(?:\.[`"]?[A-Za-z_][A-Za-z_0-9]*[`"]?)?)/i',$plain,$matches);
        $allowed=array_merge(self::$tables,['user_tab_columns','user_constraints','user_cons_columns','user_objects']);
        foreach($matches[1]as $name){$name=strtolower(str_replace(['`','"'],'',$name));if(!in_array($name,$allowed,true))throw new RuntimeException('Unfrozen table');}
        if(!$matches[1])throw new RuntimeException('No frozen SELECT table');
        return 'select';
    }
    public static function logRows(string $sql):array {
        if(!self::$case||self::$case['log_rows']<1)throw new RuntimeException('Unapproved log path');
        $plan=json_decode(file_get_contents(__DIR__.'/log-http-plan.json'),true);
        $fields=array_map(static function($f){return '`'.$f.'`';},$plan['log_SQL_contract']['insert_fields_order']);
        $prefix='INSERT INTO `ydy_initial_menu_log` ('.implode(' , ',$fields).') VALUES ';
        if(strpos($sql,$prefix)!==0)throw new RuntimeException('Log SQL exact fields/order');
        $tail=trim(substr($sql,strlen($prefix)));$rows=[];$offset=0;
        $token="'[^'\\\\]*'|-?[0-9]+";
        $row='/\\G\\(\\s*((?:'.$token.')(?:\\s*,\\s*(?:'.$token.')){10})\\s*\\)/u';
        while($offset<strlen($tail)){
            if(!preg_match($row,$tail,$match,0,$offset))throw new RuntimeException('Log literal grammar');
            $values=[];preg_match_all('/'.$token.'/u',$match[1],$pieces);
            foreach($pieces[0]as $piece)$values[]=substr($piece,0,1)==="'"?substr($piece,1,-1):(int)$piece;
            $rows[]=array_combine($plan['log_SQL_contract']['insert_fields_order'],$values);$offset+=strlen($match[0]);
            if($offset<strlen($tail)){if(!preg_match('/\\G\\s*,\\s*/',$tail,$separator,0,$offset))throw new RuntimeException('Log row separator');$offset+=strlen($separator[0]);if($offset===strlen($tail))throw new RuntimeException('Trailing separator');}
        }
        if(count($rows)!==self::$case['log_rows'])throw new RuntimeException('Log exact row budget');
        $guids=[];
        foreach($rows as $i=>$actual){foreach(self::$case['log_values'][$i]as $field=>$expected){
            if($field==='uuid'){if(!preg_match('/^[A-F0-9]{8}(-[A-F0-9]{4}){3}-[A-F0-9]{12}$/D',$actual[$field])||isset(self::$priorGuids[$actual[$field]])||isset($guids[$actual[$field]]))throw new RuntimeException('Log GUID scope');$guids[$actual[$field]]=true;}
            elseif($field==='create_time'){$t=strtotime($actual[$field]);if(date('Y-m-d H:i:s',$t)!==$actual[$field]||$t<floor(self::$started)-1||$t>time()+1)throw new RuntimeException('Log time scope');}
            else{$want=is_array($expected)?$expected['lane_'.getenv('NR_LANE')]:$expected;if((string)$actual[$field]!== (string)$want)throw new RuntimeException('Log value scope '.$field);}
        }}
        return $rows;
    }
    public static function beforeExecute(string $sql,array $bound,$params):void{
        if(!preg_match('/^INSERT\\b/i',$sql))return;
        if(self::$attempted||$bound!==[]||($params!==null&&$params!==[]))throw new RuntimeException('Log replay or unexpected binds');
        self::$inserted=self::logRows($sql);$cap=166;if(count(self::$priorGuids)+count(self::$inserted)>$cap)throw new RuntimeException('Batch log cap');
        self::$attempted=true;
    }
    public static function callBinds(string $sql,array $binds):void {
        if(strpos($sql,'CALL ')!==0)return;
        if(count($binds)!==2||($binds['1']['value']??null)!=='ZHCT_APP'||($binds['1']['type']??null)!==PDO::PARAM_STR||($binds['2']['type']??null)!==PDO::PARAM_STR||!in_array(strtolower((string)($binds['2']['value']??'')),self::$tables,true))throw new RuntimeException('Definition call scope');
    }
}
class NRStatement 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,'type'=>$type,'php_type'=>gettype($value),'ok'=>$ok];return $ok;}
    public function execute($params=null){NRGate::sql($this->queryString);NRGate::callBinds($this->queryString,$this->bound);NRGate::beforeExecute($this->queryString,$this->bound,$params);$r=['layer'=>'actual PDOStatement.execute','queryString'=>$this->queryString,'bound'=>$this->bound,'execute_params'=>$params,'success'=>false];try{$ok=parent::execute($params);$r['success']=$ok;$r['rowCount']=$this->rowCount();$r['errorInfo']=$this->errorInfo();NRGate::$journal[]=$r;return $ok;}catch(Throwable $e){$r['error']=['class'=>get_class($e),'message'=>$e->getMessage(),'errorInfo'=>$this->errorInfo()];NRGate::$journal[]=$r;throw $e;}}
}
class NRPdo extends PDO {
    public function prepare($sql,$options=[]){try{$op=NRGate::sql($sql);}catch(Throwable $e){NRGate::$journal[]=['layer'=>'native gate blocked before PDO.prepare','queryString'=>$sql,'error'=>$e->getMessage()];throw $e;}$r=['layer'=>'actual PDO.prepare','queryString'=>$sql,'operation'=>$op,'prepared'=>false];try{$s=parent::prepare($sql,$options);$r['prepared']=$s!==false;NRGate::$journal[]=$r;return $s;}catch(Throwable $e){$r['error']=['class'=>get_class($e),'message'=>$e->getMessage(),'errorInfo'=>$this->errorInfo()];NRGate::$journal[]=$r;throw $e;}}
    public function exec($sql){throw new RuntimeException('HTTP PDO.exec prohibited');}
    public function query($sql,...$args){if(!preg_match('/^SELECT\\b/i',$sql))throw new RuntimeException('PDO.query SELECT only');NRGate::sql($sql);$r=['layer'=>'actual PDO.query','queryString'=>$sql,'success'=>false];try{$s=parent::query($sql,...$args);$r['success']=$s!==false;$r['rowCount']=$s?$s->rowCount():null;NRGate::$journal[]=$r;return $s;}catch(Throwable $e){$r['error']=['class'=>get_class($e),'message'=>$e->getMessage()];NRGate::$journal[]=$r;throw $e;}}
    public function beginTransaction(){throw new RuntimeException('HTTP transaction prohibited');}
}
class NRNoPdo extends NRPdo {public function __construct(){}}
