<?php
declare(strict_types=1);
class NFGate {
 public static $journal=[],$case=[],$dml=0,$lane='';
 public static $expressions=['12.346'=>'CAST(12.345999717712402 AS REAL)','123.4568'=>'CAST(123.45680236816406 AS REAL)'];
 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 $fields=['name','set_uuid','use_weight','use_number','use_weight_unit','use_number_unit','use_time','use_method','employ','is_nutrition_station','supplier','nutrition_category_uuid','is_own'];
 public static function sql($sql){
  if(preg_match('/^CALL SP_TABLEDEF\(\?, \?\)$/D',$sql))return 'metadata';
  if(preg_match('/^SHOW (?:FULL )?COLUMNS FROM \x60?(ydy_[a-z_]+)\x60?$/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+([\x60"]?[a-z_][a-z_0-9]*[\x60"]?(?:\.[\x60"]?[a-z_][a-z_0-9]*[\x60"]?)?)/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(["\x60",'"'],'',$name)),$allowed,true))throw new RuntimeException('Unfrozen SELECT table');return 'select';}
  if((self::$case['path']??'')!=='/m/nutrition/update')throw new RuntimeException('Non-update cannot write');
  if(!is_string(self::$case['params']['use_weight']??null)||!array_key_exists(self::$case['params']['use_weight'],self::$expressions))throw new RuntimeException('Only approved new weight vectors');
  $flat=str_replace(["\x60",'"'],'',$sql);
  if(!preg_match('/^\s*UPDATE\s+ydy_nutrition\s+SET\s+(.+?)\s+WHERE\s+uuid\s*=\s*:where_AND_uuid\s*$/iD',$flat,$m))throw new RuntimeException('Exact nutrition UPDATE WHERE');
  $pairs=explode(',',$m[1]);$cols=[];
  foreach($pairs as $pair){if(self::$lane==='C'&&trim($pair)==='use_weight='.(self::$expressions[self::$case['params']['use_weight']??'']??'MISSING')){$cols[]='use_weight';continue;}if(!preg_match('/^\s*([a-z_]+)\s*=\s*:data__\1\s*$/D',$pair,$p)||self::$lane==='C'&&$p[1]==='use_weight')throw new RuntimeException('Only exact bound SET or approved C REAL');$cols[]=$p[1];}
  $expect=self::$fields;sort($expect);$actual=$cols;sort($actual);if($actual!==$expect)throw new RuntimeException('Exact 13 SET columns');
  return 'update';
 }
 public static function binds($sql,$binds,$params=null){
  if($params!==null)throw new RuntimeException('No implicit execute params');
  $op=self::sql($sql);$values=[];foreach($binds as $k=>$v)$values[ltrim((string)$k,':')]=$v;
  if($op==='metadata'&&strpos($sql,'CALL ')===0){$v=array_column(array_values($values),'value');if(count($v)!==2||$v[0]!=='ZHCT_APP'||!in_array(strtolower((string)$v[1]),self::$tables,true))throw new RuntimeException('Metadata CALL scope');return;}
  if($op!=='update')return;
  $expected=self::$case['params'];$expected['is_own']=1;$uuid=$expected['uuid'];unset($expected['uuid']);$keys=[];
  foreach(self::$fields as $f){if(self::$lane==='C'&&$f==='use_weight'){if((self::$case['C_generated_expression']??null)!==(self::$expressions[$expected[$f]]??null))throw new RuntimeException('Exact finite REAL expression');continue;}$key='data__'.$f;$keys[]=$key;$v=$values[$key]??null;$type=in_array($f,['use_weight','use_number','employ','is_nutrition_station','is_own'],true)?PDO::PARAM_INT:PDO::PARAM_STR;if(!$v||$v['value']!==$expected[$f]||$v['php_type']!==gettype($expected[$f])||$v['type']!==$type||!$v['ok'])throw new RuntimeException('Exact binding '.$f);}
  $keys[]='where_AND_uuid';$actual=array_keys($values);sort($actual);sort($keys);if($actual!==$keys)throw new RuntimeException('Exact bind set');
  $w=$values['where_AND_uuid'];if($w['value']!==$uuid||$w['type']!==PDO::PARAM_STR||$w['php_type']!=='string'||!$w['ok'])throw new RuntimeException('Exact own WHERE bind');
  if(self::$dml>=1)throw new RuntimeException('One UPDATE per request');self::$dml++;
 }
}
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,$params);$ok=parent::execute($params);$r['success']=$ok;$r['rowCount']=$this->rowCount();$r['errorInfo']=$this->errorInfo();if(NFGate::sql($this->queryString)==='update'&&$r['rowCount']!==1)throw new RuntimeException('One affected row required; no replay');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;}
}

