<?php
declare(strict_types=1);
class NFGate {
 public static $journal=[],$case=[],$own=[],$phase='',$dml=0;
 public static $tables=['ydy_nutrition','ydy_nutrition_template','ydy_nutrition_template_ext','ydy_nutrition_set','ydy_nutrition_category','ydy_nutrition_time','ydy_user','ydy_config','ydy_identity_type'];
 public static function sql($sql){
  if(preg_match('/^CALL SP_TABLEDEF\(\?, \?\)$/D',$sql))return 'metadata';
  if(preg_match('/^SHOW (?:FULL )?COLUMNS FROM `?(ydy_[a-z_]+)`?$/iD',$sql,$m)&&in_array(strtolower($m[1]),self::$tables,true))return 'metadata';
  $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)){$names=[];preg_match_all('/\b(?:FROM|JOIN)\s+([`"]?[a-z_][a-z_0-9]*[`"]?(?:\.[`"]?[a-z_][a-z_0-9]*[`"]?)?)/i',$plain,$names);$allowed=array_merge(self::$tables,['user_tab_columns','user_constraints','user_cons_columns','user_objects']);foreach($names[1]as $name)if(!in_array(strtolower(str_replace(['`','"'],'',$name)),$allowed,true))throw new RuntimeException('Unfrozen SELECT table');return 'select';}
  if(!preg_match('/^\s*(INSERT\s+INTO|UPDATE|DELETE\s+FROM)\s+[`"]?(ydy_[a-z_]+)[`"]?/i',$sql,$m))throw new RuntimeException('DML shape');
  if(empty(self::$case)||strpos(self::$case['label'],'-info')!==false)throw new RuntimeException('Read request cannot write');
  $table=strtolower($m[2]);$op=strtoupper(preg_replace('/\s+/',' ',$m[1]));
  $allowed=['ydy_nutrition'=>['INSERT INTO','UPDATE'],'ydy_nutrition_template'=>['INSERT INTO','UPDATE'],'ydy_nutrition_template_ext'=>['INSERT INTO','DELETE FROM'],'ydy_nutrition_set'=>['UPDATE'],'ydy_nutrition_category'=>['UPDATE']];
  if(!isset($allowed[$table])||!in_array($op,$allowed[$table],true))throw new RuntimeException('Frozen DML table/op');
  return $op.':'.$table;
 }
 public static function binds($sql,$binds){
  $op=self::sql($sql);$values=[];foreach($binds as $k=>$v)$values[ltrim((string)$k,':')]=$v['value'];
  if($op==='metadata'&&strpos($sql,'CALL ')===0){if(array_values($values)!==['ZHCT_APP',array_values($values)[1]??null]||!in_array(strtolower((string)(array_values($values)[1]??'')),self::$tables,true))throw new RuntimeException('Metadata CALL scope');return;}
  if(in_array($op,['select','metadata'],true))return;
  list($verb,$table)=explode(':',$op,2);
  if($verb==='INSERT INTO'){
   if($table==='ydy_nutrition_template_ext'){$uuid=self::$own['template_uuid']??'';if(!$uuid||strpos($sql,$uuid)===false&& !in_array($uuid,$values,true))throw new RuntimeException('Ext insert template owner');if(strpos($sql,'CDXNF11F5')===false&&!array_filter($values,static function($v){return is_string($v)&&strpos($v,'CDXNF11F5')===0;}))throw new RuntimeException('Ext marker');}
   else{$name='CDXNF11F5-'.self::$phase.'-'.($table==='ydy_nutrition'?'product':'template');if(!in_array($name,$values,true))throw new RuntimeException('Exact own INSERT name');foreach($values as $v)if(is_string($v)&&preg_match('/^[A-F0-9]{8}(?:-[A-F0-9]{4}){3}-[A-F0-9]{12}$/D',$v)&&strpos($v,'CF11F500-')!==0){self::$own[$table==='ydy_nutrition'?'product_uuid':'template_uuid']=$v;break;}}
  }else{
   if(!preg_match('/\bWHERE\s+[`"]?(uuid|template_uuid)[`"]?\s*=\s*(:[a-z_0-9]+|\?)\s*$/iD',$sql,$m))throw new RuntimeException('Exact UUID WHERE');
   $key=ltrim($m[2],':');$value=$m[2]==='?'?end($values):($values[$key]??null);
   $allowed=$table==='ydy_nutrition'?[self::$own['product_uuid']??'']:($table==='ydy_nutrition_template'||$table==='ydy_nutrition_template_ext'?[self::$own['template_uuid']??'']:array_slice(array_column($GLOBALS['NF_PLAN']['preparation']['rows'],'row'),1));
   if(in_array($table,['ydy_nutrition_set','ydy_nutrition_category'],true)){$allowed=[];foreach($GLOBALS['NF_PLAN']['preparation']['rows']as $r)if('ydy_'.$r['table']===$table)$allowed[]=$r['row']['uuid'];}
   if(!is_string($value)||$value===''||!in_array($value,$allowed,true))throw new RuntimeException('DML owner UUID');
  }
  if(++self::$dml>12)throw new RuntimeException('Per request DML budget');
 }
}
class NFStatement extends PDOStatement {
 private $bound=[];protected function __construct(){}
 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=['layer'=>'PDOStatement.execute','sql'=>$this->queryString,'binds'=>$this->bound,'params'=>$params,'success'=>false];try{NFGate::binds($this->queryString,$this->bound);$ok=parent::execute($params);$r['success']=$ok;$r['rowCount']=$this->rowCount();$r['errorInfo']=$this->errorInfo();return $ok;}catch(Throwable $e){$r['error']=['class'=>get_class($e),'message'=>$e->getMessage(),'errorInfo'=>$this->errorInfo()];throw $e;}finally{NFGate::$journal[]=$r;}}
}
class NFPdo extends PDO {
 public function prepare($sql,$options=[]){$r=['layer'=>'PDO.prepare','sql'=>$sql,'success'=>false];try{NFGate::sql($sql);$s=parent::prepare($sql,$options);$r['success']=$s!==false;return $s;}catch(Throwable $e){$r['error']=['class'=>get_class($e),'message'=>$e->getMessage()];throw $e;}finally{NFGate::$journal[]=$r;}}
 public function exec($sql){throw new RuntimeException('Unfrozen PDO.exec');}
 public function query($sql,...$args){NFGate::binds($sql,[]);$s=parent::query($sql,...$args);NFGate::$journal[]=['layer'=>'PDO.query','sql'=>$sql,'success'=>$s!==false];return $s;}
 public function beginTransaction(){$ok=parent::beginTransaction();NFGate::$journal[]=['layer'=>'PDO.beginTransaction','success'=>$ok];return $ok;}
 public function commit(){$ok=parent::commit();NFGate::$journal[]=['layer'=>'PDO.commit','success'=>$ok];return $ok;}
 public function rollBack(){$ok=parent::rollBack();NFGate::$journal[]=['layer'=>'PDO.rollBack','success'=>$ok];return $ok;}
}
