<?php
declare(strict_types=1);
$lane=getenv('FR_LANE');$root=getenv('FR_ROOT');$mode=$argv[1]??'';
$out=['lane'=>$lane,'method'=>'GET','read_only'=>true,'routes'=>[]];$stage='config';
function rows(PDO $db,string $sql,array $args=[]):array{if(!preg_match('/^SELECT\b/i',$sql))throw new RuntimeException('Not SELECT');$q=$db->prepare($sql);$q->execute($args);$r=$q->fetchAll(PDO::FETCH_ASSOC);if(count($r)>20000)throw new RuntimeException('Cap');foreach($r as &$v){$v=array_change_key_case($v,CASE_LOWER);foreach($v as &$x)if(is_resource($x))$x=stream_get_contents($x);ksort($v);}return $r;}
function shape($v){if(is_int($v)||is_float($v))return 'number';if(!is_array($v))return gettype($v);if(!$v)return [];if(array_keys($v)===range(0,count($v)-1))return [shape($v[0])];$r=[];foreach($v as $k=>$x)$r[$k]=shape($x);ksort($r);return $r;}
function digest($v){return hash('sha256',json_encode($v,JSON_UNESCAPED_UNICODE|JSON_INVALID_UTF8_SUBSTITUTE));}
function request(string $path,array $params,array $headers=[]):array{global $lane;$base='http://127.0.0.1:'.($lane==='B'?18090:18091);$h='';foreach($headers as $k=>$v)$h.=$k.': '.$v."\r\n";$context=stream_context_create(['http'=>['method'=>'GET','header'=>$h,'timeout'=>30,'ignore_errors'=>true]]);$body=@file_get_contents($base.$path.'?'.http_build_query($params,'','&',PHP_QUERY_RFC3986),false,$context);$j=json_decode((string)$body,true);if(!is_array($j))throw new RuntimeException('Non JSON');return ['status'=>(int)substr($http_response_header[0]??'',9,3),'json'=>$j];}
try{
 if(!in_array($lane,['B','C'],true)||!in_array($mode,['run','preflight'],true))throw new RuntimeException('Invocation');
 $cfg=require $root.'/application/database.php';$ac=$cfg['ai_zhct'];
 if($lane==='B'){$local=require '/workspace/zhct/zhctproject/store/application/config/local/database.php';foreach(['hostname','database','username','password'] as $k)if(($cfg[$k]??null)!==($local[$k]??null)||($ac[$k]??null)!==($local['ai_zhct'][$k]??null))throw new RuntimeException('Not local');}
 else{foreach(['hostname'=>'POC_HOST','hostport'=>'POC_PORT','database'=>'POC_SCHEMA','username'=>'POC_USER','password'=>'POC_PASSWORD'] as $k=>$e)if((string)$cfg[$k]!==getenv($e))throw new RuntimeException('DM config');if(getenv('POC_HOST')!=='127.0.0.1'||getenv('POC_PORT')!=='5237'||getenv('POC_AI_HOST')!=='127.0.0.1'||getenv('POC_AI_PORT')!=='5238')throw new RuntimeException('DM ports');}
 $connect=static function($c,$port){global $lane;$dsn=$lane==='B'?'mysql:host='.$c['hostname'].';port='.($c['hostport']?:3306).';dbname='.$c['database'].';charset=utf8mb4':'dm:host=127.0.0.1;port='.$port.';schema='.$c['database'];return new PDO($dsn,$c['username'],$c['password'],[PDO::ATTR_ERRMODE=>PDO::ERRMODE_EXCEPTION,PDO::ATTR_CASE=>PDO::CASE_LOWER]);};
 $s=$connect($cfg,5237);$a=$connect($ac,5238);$sp=$lane==='B'?'ydy_':'YDY_';$ap=$lane==='B'?'yoshop_':'YOSHOP_';
 $stage='source';$refunds=rows($a,'SELECT * FROM '.$ap.'cash_refund_order WHERE STATUS=20 AND IS_DELETE=0 ORDER BY AUDIT_TIME DESC,ID DESC');
 $mappedCount=(int)rows($s,'SELECT COUNT(*) AS N FROM '.$sp.'user WHERE AI_USER_ID>0')[0]['n'];
 $auth=rows($s,'SELECT ACCOUNT,PWD,'.$s->quote('unused').' AS UNUSED FROM '.$sp.'api_auth WHERE TYPE=1 AND STATUS=1 AND START_TIME<=? AND END_TIME>=?',[date('Y-m-d H:i:s'),date('Y-m-d H:i:s')]);
 $out['preflight']=['refunds'=>count($refunds),'mapped_users'=>$mappedCount,'valid_finance_auth'=>count($auth)];
 if($mode==='preflight'){echo json_encode($out),"\n";exit;}
 $sourceBefore=digest($refunds);$stage='mobile';
 if($refunds){$r=$refunds[0];$uid=(int)$r['user_id'];require $root.'/application/base/Security.php';$token=\app\base\Security::encrypt(implode("\t",[(string)$uid,'CDXFR10_READONLY','CDXFR10_READONLY','0','','','CDXFR10_READONLY']));
  $owned=array_values(array_filter($refunds,static function($v)use($uid){return (int)$v['user_id']===$uid;}));
  foreach(['list'=>[],'detail'=>['refund_id'=>(int)$r['id']],'detail_missing'=>['refund_id'=>0]] as $name=>$params){$res=request('/api/cashRefund/'.($name==='detail_missing'?'detail':$name),$params,['token'=>$token]);$j=$res['json'];$entry=['status'=>$res['status'],'code'=>$j['code']??null,'shape'=>shape($j)];
   if($name==='list'){$d=$j['data'];$entry['rows']=count($d['data']??[]);$entry['source_match']=($d['total']??-1)===count($owned)&&count($d['data'])===min(15,count($owned));foreach($d['data'] as $i=>$v){$vsrc=$owned[$i];$entry['source_match']=$entry['source_match']&&$v['id']===(int)$vsrc['id']&&$v['refund_no']===$vsrc['refund_no']&&$v['refund_amount']===number_format((float)$vsrc['refund_amount'],2,'.','')&&(string)$v['audit_time']===(string)$vsrc['audit_time'];}}
   elseif($name==='detail'){$v=$j['data'];$entry['source_match']=true;$entry['source_field_types']=[];foreach($r as $key=>$val){$entry['source_field_types'][$key]=gettype($val);$expected=$val;if(in_array($key,['id','user_id','store_id','is_delete','balance_log_id','status'],true))$expected=(int)$val;elseif(in_array($key,['apply_balance','refund_amount','apply_after_balance','actual_deduct_amount'],true))$expected=number_format((float)$val,2,'.','');elseif(in_array($key,['audit_balance','after_balance'],true))$expected=$val===null?'--':number_format((float)$val,2,'.','');$entry['source_match']=$entry['source_match']&&array_key_exists($key,$v)&&$v[$key]===$expected;}$entry['source_fields_checked']=count($r);}
   else{$entry['source_match']=($j['code']??null)===1&&($j['data']??null)===[];}
   $out['routes']['GET /api/cashRefund/'.$name]=$entry;
  }
 }
 $stage='visitor';
 if($auth){$credential=rows($s,'SELECT * FROM '.$sp.'api_auth WHERE TYPE=1 AND STATUS=1 AND START_TIME<=? AND END_TIME>=? ORDER BY ID DESC',[date('Y-m-d H:i:s'),date('Y-m-d H:i:s')])[0];$headers=['account'=>$credential['account'],'pwd'=>$credential['pwd'],'key'=>$credential['key']];
  $params=['start_time'=>'2020-01-01 00:00:00','end_time'=>'2030-01-01 00:00:00','page_size'=>3];
  // Scope to the selected natural customer's mobile without editing balances or profiles.
  if(isset($uid)){$profiles=rows($s,'SELECT STAFF_UUID,MOBILE FROM '.$sp.'user WHERE AI_USER_ID=? ORDER BY IS_DELETE DESC,ID ASC',[$uid]);if($profiles){$profile=end($profiles);$staff=rows($s,'SELECT MOBILE FROM '.$sp.'staff WHERE UUID=?',[$profile['staff_uuid']]);$aiUser=rows($a,'SELECT MOBILE FROM '.$ap.'user WHERE USER_ID=?',[$uid]);foreach([$staff[0]['mobile']??null,$profile['mobile']??null,$aiUser[0]['mobile']??null] as $candidate)if(trim((string)$candidate)!==''){$params['mobile']=trim((string)$candidate);break;}}}
  foreach(['first'=>$params,'invalid_date'=>array_replace($params,['start_time'=>'bad']),'invalid_page'=>array_replace($params,['page_size'=>0])] as $name=>$p){$res=request('/api/finance/visitor/flows',$p,$headers);$j=$res['json'];$entry=['status'=>$res['status'],'code'=>$j['code']??null,'shape'=>shape($j)];
   if($name==='first'&&($j['code']??-1)===0){$d=$j['data'];$entry['rows']=count($d['list']);$entry['total']=$d['total'];$entry['source_match']=true;$entry['amount_string']=true;$source=[];
    // Read only the first narrow source time window returned, not the whole historical ledger.
    if($d['list']){$times=array_map(static function($v){return strtotime($v['occur_time']);},$d['list']);$min=min($times);$max=max($times);
    foreach(['cash'=>'user_balance_log','subsidy'=>'user_subsidy_log'] as $fund=>$table)foreach(rows($a,'SELECT LOG_ID,USER_ID,MONEY,CREATE_TIME,STORE_ID,SCENE FROM '.$ap.$table.' WHERE STORE_ID=10001 AND CREATE_TIME>=? AND CREATE_TIME<=?',[$min,$max]) as $v){$no='VF'.strtoupper(substr(hash('sha256','visitor-flow-v1|'.$fund.'|10001|'.$v['log_id']),0,30));$source[$no]=$v;}}
    foreach($d['list'] as $v){$vsrc=$source[$v['flow_no']]??null;$entry['source_match']=$entry['source_match']&&$vsrc!==null&&$v['amount']===number_format((float)$vsrc['money'],2,'.','')&&$v['occur_time']===date('Y-m-d H:i:s',(int)$vsrc['create_time']);$entry['amount_string']=$entry['amount_string']&&is_string($v['amount']);}
    if($d['next_cursor']){$next=request('/api/finance/visitor/flows',['cursor'=>$d['next_cursor'],'page_size'=>3],$headers);$entry['next_page_status']=$next['status'];$entry['next_page_code']=$next['json']['code']??null;$entry['next_page_rows']=count($next['json']['data']['list']??[]);$entry['next_page_disjoint']=count(array_intersect(array_column($d['list'],'flow_no'),array_column($next['json']['data']['list']??[],'flow_no')))===0;}
   }elseif($name!=='first'){$entry['source_match']=$res['status']===400&&($j['code']??0)===1;}
   $out['routes']['GET /api/finance/visitor/flows '.$name]=$entry;
  }
 }else{$out['visitor_limit']='No valid type-1 API auth; not changed';}
 $stage='after';$out['source_unchanged']=digest(rows($a,'SELECT * FROM '.$ap.'cash_refund_order WHERE STATUS=20 AND IS_DELETE=0 ORDER BY AUDIT_TIME DESC,ID DESC'))===$sourceBefore;
 $out['actor_writes']=0;$out['ledger_writes']=0;$out['failure']='';
}catch(Throwable $e){$out['failure']=['stage'=>$stage,'class'=>get_class($e),'message_sha12'=>substr(hash('sha256',$e->getMessage()),0,12)];}
if(isset($a,$refunds,$sourceBefore)){$out['source_unchanged']=digest(rows($a,'SELECT * FROM '.$ap.'cash_refund_order WHERE STATUS=20 AND IS_DELETE=0 ORDER BY AUDIT_TIME DESC,ID DESC'))===$sourceBefore;}
echo json_encode($out,JSON_UNESCAPED_UNICODE|JSON_INVALID_UTF8_SUBSTITUTE),"\n";exit(empty($out['failure'])?0:2);
