<?php
declare(strict_types=1);
// A deliberately limited validator for the frozen plain INSERT/primary-key UPDATE
// emitted by StaffPower. Unsupported syntax fails closed; no SQL enters public reports.
final class DbWriteAudit
{
    private $runtime;
    public function __construct(string $runtime){$this->runtime=$runtime;}
    private static function parts(string $input):array
    {
        $out=[];$buf='';$quote='';$length=strlen($input);
        for($i=0;$i<$length;$i++){$c=$input[$i];
            if($quote!==''){$buf.=$c;if($c==='\\'&&$i+1<$length){$buf.=$input[++$i];continue;}
                if($c===$quote){if($i+1<$length&&$input[$i+1]===$quote){$buf.=$input[++$i];continue;}$quote='';}continue;}
            if($c==="'"||$c==='"'||$c==='`'){$quote=$c;$buf.=$c;continue;}
            if($c===','){$out[]=trim($buf);$buf='';continue;}$buf.=$c;
        }
        if($quote!=='')throw new RuntimeException('Unsupported unclosed SQL audit literal');$out[]=trim($buf);return $out;
    }
    private static function scalar(string $v):string
    {
        $v=trim($v);if(strlen($v)>1&&$v[0]==="'"&&substr($v,-1)==="'")$v=substr($v,1,-1);
        if(!preg_match('/^-?[0-9]+(?:\.[0-9]+)?$/D',$v))throw new RuntimeException('Non-numeric owner audit literal');return $v;
    }
    private static function owner(string $literal,int $expected):bool
    {
        $v=self::scalar($literal);return preg_match('/^[0-9]+$/D',$v)===1&&(int)$v===$expected&&$expected>0;
    }
    public function observe($sql):void
    {
        $sql=(string)$sql;if(!preg_match('/^\s*(INSERT|UPDATE|DELETE|REPLACE|MERGE|TRUNCATE|ALTER|DROP|CREATE)\b/i',$sql,$m))return;
        $op=strtoupper($m[1]);$map=json_decode((string)file_get_contents($this->runtime.'/private-owned-map.json'),true);$pass=false;$reason='Unsupported write shape';$table='unknown';
        try{
            if($op==='INSERT'&&preg_match('/^\s*INSERT\s+INTO\s+([^\s(]+)\s*\((.*?)\)\s*VALUES\s*\((.*)\)\s*;?\s*$/is',$sql,$p)){
                $table=strtolower(str_replace(['`','"'],'',$p[1]));
                $cols=self::parts($p[2]);$values=self::parts($p[3]);if(count($cols)!==count($values))throw new RuntimeException('Audit arity');$data=[];
                foreach($cols as $i=>$col)$data[strtolower(trim($col,' `"'))]=$values[$i];
                $pass=in_array($table,['yoshop_meal_recommend','yoshop_meal_recommend_history','yoshop_staff_weight'],true)
                    &&isset($data['staff_id'],$data['store_id'])&&self::owner($data['staff_id'],(int)($map['staff_id']??0))
                    &&self::owner($data['store_id'],2147000921);
                $reason='Explicit inserted staff and store owner';
            }elseif($op==='INSERT'&&preg_match('/^\s*INSERT\s+INTO\s+([^\s]+)\s+SET\s+(.*?)\s*;?\s*$/is',$sql,$p)){
                $table=strtolower(str_replace(['`','"'],'',$p[1]));$data=[];
                foreach(self::parts($p[2]) as $assignment){if(!preg_match('/^\s*[`"]?([a-z0-9_]+)[`"]?\s*=\s*(.*)$/iD',$assignment,$a))throw new RuntimeException('Unsupported insert-set assignment');$data[strtolower($a[1])]=$a[2];}
                $pass=in_array($table,['yoshop_meal_recommend','yoshop_meal_recommend_history','yoshop_staff_weight'],true)
                    &&isset($data['staff_id'],$data['store_id'])&&self::owner($data['staff_id'],(int)($map['staff_id']??0))
                    &&self::owner($data['store_id'],2147000921);
                $reason='Explicit insert-set staff and store owner';
            }elseif($op==='UPDATE'&&preg_match('/^\s*UPDATE\s+([^\s]+)\s+SET\s+(.*?)\s+WHERE\s+(.*?)\s*;?\s*$/is',$sql,$p)){
                $table=strtolower(str_replace(['`','"'],'',$p[1]));
                $where=trim(str_replace(['(',')','`','"'],'',$p[3]));
                if(!in_array($table,['yoshop_staff','yoshop_staff_weight','yoshop_meal_recommend'],true))throw new RuntimeException('Forbidden weight update table');
                if(!preg_match('/^(?:'.preg_quote($table,'/').'\.)?store_id\s*=\s*\x27?2147000921\x27?\s+AND\s+id\s*=\s*(\x27?[0-9]+\x27?)\s*$/iD',$where,$w))throw new RuntimeException('Non-owned scoped primary-key update');
                $id=(int)self::scalar($w[1]);$pass=$table==='yoshop_meal_recommend'?in_array($id,$map['current_ids']??[],true):($table==='yoshop_staff_weight'?in_array($id,$map['weight_ids']??[],true):$id===(int)($map['staff_id']??0));
                foreach(self::parts($p[2]) as $assignment){if(preg_match('/^\s*[`"]?id[`"]?\s*=/i',$assignment)){$pass=false;continue;}
                    if(!preg_match('/^\s*[`"]?(staff_id|store_id)[`"]?\s*=\s*(.*)$/i',$assignment,$a))continue;
                    $expected=strtolower($a[1])==='staff_id'?(int)$map['staff_id']:2147000921;$pass=$pass&&self::owner($a[2],$expected);}
                $reason='Only independently verified owned current primary key';
            }
        }catch(Throwable $e){$reason=$e->getMessage();$pass=false;}
        file_put_contents($this->runtime.'/private-dml-audit-sql.jsonl',json_encode(['sql'=>$sql,'pass'=>$pass])."\n",FILE_APPEND|LOCK_EX);
        file_put_contents($this->runtime.'/write-audit.jsonl',json_encode(['operation'=>$op,'table'=>$table,'owned_scope_verified'=>$pass,'reason'=>$reason])."\n",FILE_APPEND|LOCK_EX);
        if(!$pass)throw new RuntimeException('Frozen write audit rejected '.$op.' '.$table.' '.$reason);
    }
}
