<?php
final class FoodGate {
 public static $journal=[],$case=[],$dml=0,$generatedId=null,$lane='',$dry=false;
 public const READ_TABLES=['ydy_config','ydy_user','ydy_menu','ydy_role','ydy_role_data','ydy_user_data','ydy_department','ydy_staff','ydy_staff_department','ydy_project','ydy_restaurants','ydy_restaurant_area','ydy_restaurant_shop','ydy_dishes_import_task','ydy_annex','ydy_employment_state','ydy_enginery_iochemistry'];
 public static function readonlySql($sql,$role){if($role!=='store')throw new RuntimeException('Read8 only original primary');if($sql==='CALL SP_TABLEDEF(?, ?)')return 'read';if(preg_match('/^SHOW (?:FULL )?COLUMNS FROM [`"]?(ydy_[a-z_]+)[`"]?$/iD',$sql,$m)){if(!in_array(strtolower($m[1]),self::READ_TABLES,true))throw new RuntimeException('Read8 metadata table');return 'read';}$plain=preg_replace("/'(?:''|[^'])*'/s","''",$sql);if(!preg_match('/^\s*SELECT\b/i',$sql)||preg_match('/;|--|\/\*|\b(MERGE|DROP|ALTER|CREATE|TRUNCATE|CALL|EXEC|DO|INTO|OUTFILE|SLEEP|BENCHMARK|DBLINK)\b/i',$plain))throw new RuntimeException('Read8 hard readonly');$m=[];preg_match_all('/\b(?:FROM|JOIN)\s+([`"]?[a-z_][a-z_0-9]*[`"]?)/i',$plain,$m);foreach($m[1]as $t)if(!in_array(strtolower(str_replace(['`','"'],'',$t)),array_merge(self::READ_TABLES,['user_tab_columns','user_constraints','user_cons_columns','dual']),true))throw new RuntimeException('Read8 original table whitelist');return 'read';}
 public static function sql($sql,$role){if($sql==='CALL SP_TABLEDEF(?, ?)')return 'read';if(preg_match('/^SHOW (?:FULL )?COLUMNS FROM [`"]?(ydy_[a-z_]+)[`"]?$/iD',$sql,$m)){if(!in_array($m[1],['ydy_config','ydy_foods_composition','ydy_foods_cate','ydy_user'],true))throw new RuntimeException('Metadata scope');return 'read';}$plain=preg_replace("/'(?:''|[^'])*'/s","''",$sql);if(preg_match('/;|--|\/\*|\b(MERGE|DROP|ALTER|CREATE|TRUNCATE|CALL|EXEC|OUTFILE|SLEEP|BENCHMARK|DBLINK)\b/i',$plain))throw new RuntimeException('Unsafe SQL token');if(preg_match('/^\s*SELECT\b/i',$sql)){$m=[];preg_match_all('/\b(?:FROM|JOIN)\s+([`"]?[a-z_][a-z_0-9]*[`"]?)/i',$plain,$m);foreach($m[1]as $t)if(!in_array(strtolower(str_replace(['`','"'],'',$t)),['ydy_config','ydy_foods_composition','ydy_foods_cate','ydy_user','user_tab_columns','user_constraints','user_cons_columns','dual'],true))throw new RuntimeException('Native read table scope');return 'read';}if($role!=='store')throw new RuntimeException('No AI DB writes');return 'write';}
 public static function execute($sql,$binds,$params,$role){$read=!empty(self::$case['readonly']);$op=$read?self::readonlySql($sql,$role):self::sql($sql,$role);if($params!==null)throw new RuntimeException('Explicit typed binds only');if($sql==='CALL SP_TABLEDEF(?, ?)'){$v=array_column($binds,'value');$allowed=$read?array_map('strtoupper',self::READ_TABLES):['YDY_CONFIG','YDY_FOODS_COMPOSITION','YDY_FOODS_CATE','YDY_USER'];if(count($v)!==2||$v[0]!=='ZHCT_APP'||!in_array($v[1],$allowed,true))throw new RuntimeException('Metadata exact table/schema');}if($op==='read'){if(!self::$dry)($GLOBALS['FOOD_BUDGET'])('business-read',800);return;}
  if(self::$dml||empty(self::$case['allowed']))throw new RuntimeException('Only one prescribed Food DML');$s=strtolower(preg_replace('/\s+/',' ',str_replace(['`','"'],'',trim($sql))));$lookup=array_change_key_case($binds,CASE_LOWER);$used=[];$get=static function($v)use($lookup,&$used){$v=trim($v);if(!preg_match('/^:[a-z0-9_]+$/D',$v)||!isset($lookup[$v]))throw new RuntimeException('Bound scalar only');$used[]=$v;return $lookup[$v]['value'];};$kind='';$data=[];$id=null;
  if(preg_match('/^insert into ydy_foods_composition \(([^)]+)\) values \(([^)]+)\)$/D',$s,$m)){$kind='insert';$cols=array_map('trim',explode(',',$m[1]));$vals=array_map($get,explode(',',$m[2]));if(count($cols)!==count($vals))throw new RuntimeException('Insert arity');$data=array_combine($cols,$vals);}
  elseif(preg_match('/^update ydy_foods_composition set (.+) where id = (:[a-z_0-9]+)$/D',$s,$m)){$kind='update';foreach(explode(',',$m[1])as $pair){if(!preg_match('/^\s*([a-z_]+)\s*=\s*(:[a-z_0-9]+)$/D',$pair,$x))throw new RuntimeException('Exact assignments');$data[$x[1]]=$get($x[2]);}$id=$get($m[2]);}
  elseif(preg_match('/^delete from ydy_foods_composition where id in \((:[a-z_0-9]+)\)$/D',$s,$m)){$kind='delete';$id=$get($m[1]);}
  else throw new RuntimeException('Only exact Food table grammar');$allow=self::$case['allowed'];if($kind!==$allow['kind']||($kind!=='insert'&&(string)$id!==(string)$allow['id']))throw new RuntimeException('Own exact identity');if($kind!=='delete'){if(!isset($data['create_time'])||!is_string($data['create_time'])||abs(time()-strtotime($data['create_time']))>90)throw new RuntimeException('Original dynamic create_time');unset($data['create_time']);}$want=$allow['data'];ksort($want);ksort($data);if($want!==$data)throw new RuntimeException('Exact original planned Food field values and PHP types');if(count($used)!==count($binds))throw new RuntimeException('No extra bindings');
  $numeric=['edible_part','energy','protein','fat','carbohydrate','dietary_fiber','category','minute_sort','reclassify','category_two','category_three'];foreach($binds as $key=>$b){$f=preg_replace('/^:data__|^:where_AND_/','',$key);$type=in_array($f,$numeric,true)||strpos($f,'id')===0?1:2;if($b['type']!==$type||$b['ok']!==true||!in_array($b['php_type'],['string','integer','double'],true))throw new RuntimeException('Original native PDO/PHP bind type');}if(!self::$dry)($GLOBALS['FOOD_BUDGET'])('DML-'.$kind,1);self::$dml++;
 }
}
class FoodStatement extends PDOStatement {
 private $role,$bound=[];protected function __construct($role){$this->role=$role;}
 public function bindValue($p,$v,$t=PDO::PARAM_STR){$ok=parent::bindValue($p,$v,$t);$this->bound[(string)$p]=['value'=>$v,'type'=>$t,'php_type'=>gettype($v),'ok'=>$ok];return $ok;}
 public function execute($params=null){$r=['schema'=>$this->role,'sql'=>$this->queryString,'binds'=>$this->bound,'execute_parameters'=>$params,'success'=>false];try{FoodGate::execute($this->queryString,$this->bound,$params,$this->role);$ok=parent::execute($params);$r['success']=$ok;$r['affected']=$this->rowCount();$r['errorInfo']=$this->errorInfo();return $ok;}catch(Throwable $e){$r['error']=['class'=>get_class($e),'message'=>$e->getMessage()];throw $e;}finally{FoodGate::$journal[]=$r;}}
}
class FoodPDO extends PDO {
 public $schema;
 public function prepare($sql,$options=[]){if(!empty(FoodGate::$case['readonly']))FoodGate::readonlySql($sql,$this->schema);else FoodGate::sql($sql,$this->schema);return parent::prepare($sql,$options);}
 public function query($sql,...$args){throw new RuntimeException('Only original prepared SQL');}
 public function exec($sql){throw new RuntimeException('No extra direct write');}
 public function beginTransaction(){throw new RuntimeException('Optional sync transaction not allowed');}
 public function lastInsertId($seq=null){$v=parent::lastInsertId($seq);FoodGate::$generatedId=$v;FoodGate::$journal[]=['layer'=>'PDO.lastInsertId','schema'=>$this->schema,'sequence'=>$seq,'value'=>$v,'php_type'=>gettype($v)];return $v;}
}
