<?php
declare(strict_types=1);
final class ConfigWriteAudit
{
    private $runtime;private $tenant;
    public function __construct(string $runtime,int $tenant){if(!in_array($tenant,[2147000721,2147000722],true))throw new RuntimeException('Audit tenant');$this->runtime=$runtime;$this->tenant=$tenant;}
    private static function parts(string $s):array{$out=[];$buf='';$quote='';$n=strlen($s);for($i=0;$i<$n;$i++){$c=$s[$i];
        if($quote!==''){$buf.=$c;if($c==='\\'&&$i+1<$n){$buf.=$s[++$i];continue;}if($c===$quote){if($i+1<$n&&$s[$i+1]===$quote){$buf.=$s[++$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('Audit unclosed literal');$out[]=trim($buf);return $out;}
    private static function id(string $s):int{$s=trim($s);if(strlen($s)>1&&$s[0]==="'"&&substr($s,-1)==="'")$s=substr($s,1,-1);
        if(!preg_match('/^[0-9]+$/D',$s)||(int)$s<=0)throw new RuntimeException('Audit nonpositive/noninteger owner');return (int)$s;}
    private static function assignments(string $s):array{$out=[];foreach(self::parts($s) as $part){if(!preg_match('/^\s*[`"]?([a-z0-9_]+)[`"]?\s*=\s*(.*)$/iD',$part,$m))throw new RuntimeException('Audit unsupported assignment');$key=strtolower($m[1]);if(isset($out[$key]))throw new RuntimeException('Audit duplicate field');$out[$key]=$m[2];}return $out;}
    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]);$table='unknown';$pass=false;$reason='Unsupported frozen config DML';
        try{
            $map=json_decode((string)file_get_contents($this->runtime.'/private-owned-map.json'),true);$data=[];
            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');
                foreach($cols as $i=>$col){$key=strtolower(trim($col,' `"'));if(isset($data[$key]))throw new RuntimeException('Audit duplicate field');$data[$key]=$values[$i];}
            }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=self::assignments($p[2]);
            }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]));$pk=$table==='yoshop_user_grade'?'grade_id':'plan_id';$where=trim(str_replace(['(',')','`','"'],'',$p[3]));
                $atoms=preg_split('/\s+AND\s+/i',$where);if(count($atoms)!==2)throw new RuntimeException('Audit requires exact store AND primary key');$keys=[];
                foreach($atoms as $atom){if(!preg_match('/^(?:'.preg_quote($table,'/').'\.)?(store_id|'.$pk.')\s*=\s*(\x27?[0-9]+\x27?)\s*$/iD',trim($atom),$a))throw new RuntimeException('Audit unsupported WHERE');
                    $key=strtolower($a[1]);if(isset($keys[$key]))throw new RuntimeException('Audit duplicate WHERE');$keys[$key]=self::id($a[2]);}
                $pass=isset($keys['store_id'],$keys[$pk])&&$keys['store_id']===$this->tenant&&($map['rows'][$table][(string)$keys[$pk]]??null)===$this->tenant;
                $data=self::assignments($p[2]);if(isset($data[$pk]))$pass=false;if(isset($data['store_id']))$pass=$pass&&self::id($data['store_id'])===$this->tenant;
                $reason='Exact own store AND independently verified config primary key';
            }
            if($op==='INSERT'){$pass=isset($data['store_id'])&&self::id($data['store_id'])===$this->tenant;$reason='Explicit own inserted config store';}
            $pass=$pass&&in_array($table,['yoshop_user_grade','yoshop_recharge_plan'],true);
        }catch(Throwable $e){$pass=false;$reason=$e->getMessage();}
        file_put_contents($this->runtime.'/private-write-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,'tenant'=>$this->tenant,'owned_scope_verified'=>$pass,'reason'=>$reason])."\n",FILE_APPEND|LOCK_EX);
        if(!$pass)throw new RuntimeException('Config write audit rejected '.$op.' '.$table.' '.$reason);
    }
}
