<?php
declare(strict_types=1);
// Independent original-table projection. No ORM/model/getter is called here.
function rmWireRow(array $r,bool $update=true):array
{
    foreach($update?['create_time','update_time']:['create_time'] as $f){if(array_key_exists($f,$r)){$r[$f]=empty($r[$f])?null:date('Y-m-d H:i:s',(int)$r[$f]);}}
    return $r;
}
function rmTree(array $rows,string $pk,int $parent=0,array $path=[]):array
{
    $out=[];foreach($rows as $r){if($r['parent_id']===$parent){if(in_array($r[$pk],$path,true)){throw new RuntimeException('Natural tree cycle');}$child=rmTree($rows,$pk,$r[$pk],array_merge($path,[$r[$pk]]));if($child){$r['children']=$child;}$out[]=$r;}}
    return $out;
}
function rmMenuProjection(PDO $db):array
{
    return rmMenuFromRows(cpRows($db,'SELECT * FROM yoshop_store_menu ORDER BY sort ASC,create_time ASC'),cpRows($db,'SELECT * FROM yoshop_store_menu_api'));
}
function rmMenuFromRows(array $rows,array $rel):array
{
    $parts=[];
    foreach($rows as $r){$id=$r['menu_id'];foreach(['action_mark','sort','create_time','update_time'] as $f){unset($r[$f]);}$r['menuApi']=[];foreach($rel as $v){if($v['menu_id']===$id){$r['menuApi'][]=rmWireRow($v,false);}}$r['apiIds']=array_column($r['menuApi'],'api_id');$parts[]=$r;}
    $filter=static function(array $nodes)use(&$filter):array{$out=[];foreach($nodes as $r){if(!$r['is_page']&&empty($r['children'])){continue;}if(!empty($r['children'])){$r['children']=$filter($r['children']);}$out[]=$r;}return $out;};
    return ['expected'=>$filter(rmTree($parts,'menu_id')),'source_rows'=>$rows,'relation_rows'=>$rel];
}
function rmRoleProjection(PDO $db,int $tenant,?string $search):array
{
    $where=' WHERE store_id=?';$params=[$tenant];if($search!==null&&$search!==''){$where.=' AND role_name LIKE ?';$params[]='%'.$search.'%';}
    return rmRoleFromRows(cpRows($db,'SELECT * FROM yoshop_store_role'.$where.' ORDER BY sort ASC,create_time ASC',$params),cpRows($db,'SELECT * FROM yoshop_store_role_menu WHERE store_id=?',[$tenant]));
}
function rmRoleFromRows(array $rows,array $rel):array
{
    $parts=[];
    foreach($rows as $r){$r=rmWireRow($r);$r['roleMenu']=[];foreach($rel as $v){if($v['role_id']===$r['role_id']){$r['roleMenu'][]=rmWireRow($v,false);}}$r['menuIds']=array_column($r['roleMenu'],'menu_id');$parts[]=$r;}
    return ['expected'=>rmTree($parts,'role_id'),'source_rows'=>$rows,'relation_rows'=>$rel];
}
function rmStrict($a,$b,string $path='$'):?string
{
    if(gettype($a)!==gettype($b)){return $path.':type';}
    if(is_object($a)){$a=get_object_vars($a);$b=get_object_vars($b);ksort($a);ksort($b);}
    if(is_array($a)){if(array_keys($a)!==array_keys($b)){return $path.':keys';}foreach($a as $k=>$v){if(($err=rmStrict($v,$b[$k],$path.'.'.$k))!==null){return $err;}}return null;}
    return $a===$b?null:$path.':value';
}
