#!/usr/bin/env python3
import copy,hashlib,json,re
from decimal import Decimal
from datetime import datetime
from pathlib import Path
from zoneinfo import ZoneInfo
ROOT=Path(__file__).resolve().parent
META=json.loads((ROOT/'structure-B.json').read_text())['tables'];TZ=ZoneInfo('Asia/Shanghai')

def equal(a,b,p='$'):
 if type(a)!=type(b):raise AssertionError(f'{p} type {type(a).__name__}!={type(b).__name__}')
 if isinstance(a,dict):
  if a.keys()!=b.keys():raise AssertionError(f'{p} keys {a.keys() ^ b.keys()}')
  for k in a:equal(a[k],b[k],p+'.'+k)
 elif isinstance(a,list):
  if len(a)!=len(b):raise AssertionError(f'{p} length {len(a)}!={len(b)}')
  for i,(x,y) in enumerate(zip(a,b)):equal(x,y,f'{p}[{i}]')
 elif a!=b:raise AssertionError(f'{p} {a!r}!={b!r}')

def num(v):
 x=float(v);return int(x) if x.is_integer() else x
def money(v):return format(Decimal(str(v)).quantize(Decimal('.01')),'f')
def date(v):return datetime.fromtimestamp(int(v),TZ).strftime('%Y-%m-%d %H:%M:%S')
def cast(row,table,lane,phase):
 out={}
 for c in META[table]['columns']:
  k=c['column_name'];v=row[k];t=c['data_type']
  if v is None:out[k]=None
  elif k in ('create_time','update_time'):out[k]=date(v)
  elif t=='date':out[k]=str(v)[:10]
  elif 'int' in t:out[k]=int(v)
  elif t=='decimal':out[k]=money(v) if lane=='B' or phase=='FINAL' else num(v)
  elif t in ('float','double'):out[k]=num(v)
  else:out[k]=v
 return out

def file(row,module,lane,phase):
 d=cast(row,'upload_file',lane,phase);url=f'http://{module}.order-read.invalid/uploads/{row["file_path"]}';d['preview_url']=d['external_url']=url;return d
def user(row,original,module,lane,phase,avatar):
 d=cast(row,'user',lane,phase)
 if avatar:
  f=next(r for r in original['upload_file'] if int(r['file_id'])==int(row['avatar_id']));im=file(f,module,lane,phase);d['avatar_url']=im['preview_url'];d['avatar']=im
 return d
def goods(row,original,module,lane,phase):
 d=cast(row,'order_goods',lane,phase);d.pop('content');d['goods_props']=json.loads(row['goods_props']);im=file(next(r for r in original['upload_file'] if int(r['file_id'])==int(row['image_id'])),module,lane,phase);d['goods_image']=im['preview_url'];d['image']=im;return d
def order(row,original,module,lane,phase,detail):
 d=cast(row,'order',lane,phase);d['order_price']=money(row['order_price'] if Decimal(str(row['order_price'])) else Decimal(str(row['total_price']))-Decimal(str(row['coupon_money']))+Decimal(str(row['update_price'])))
 d['update_price']={'symbol':'-' if Decimal(str(row['update_price']))<0 else '+','value':money(abs(Decimal(str(row['update_price']))))}
 for k in ('pay_time','delivery_time','receipt_time'):d[k]=date(row[k]) if int(row[k]) else ''
 d['order_source_data']=json.loads(row['order_source_data']) or [];d['state_text']='已取消';assert int(row['order_status'])==20
 u=next(r for r in original['user'] if int(r['user_id'])==int(row['user_id']));d['user']=user(u,original,module,lane,phase,not detail)
 d['goods']=[goods(r,original,module,lane,phase) for r in original['order_goods'] if int(r['order_id'])==int(row['order_id'])]
 assert not [r for r in original['order_address'] if int(r['order_id'])==int(row['order_id'])];d['address']=None
 if detail:
  assert not original['order_delivery'] and not original['payment_trade'] and not original['express'];d.update(express=None,delivery=[],trade=None)
 return d
def recharge(row,original,module,lane,phase):
 d=cast(row,'recharge_order',lane,phase);d['pay_time']=date(row['pay_time']) if int(row['pay_time']) else ''
 u=next(r for r in original['user'] if int(r['user_id'])==int(row['user_id']));d['user']=user(u,original,module,lane,phase,True)
 plan=next(r for r in original['recharge_order_plan'] if int(r['order_id'])==int(row['order_id']));d['order_plan']=cast(plan,'recharge_order_plan',lane,phase);return d
def expected_data(w,c,lane,phase):
 o=w['original'];module=c['module'];tenant=2147001111 if module=='store' else 2147001112;p=c['params'] or {};path=c['path']
 if path.endswith('/order/detail'):
  row=next(r for r in o['order'] if int(r['order_id'])==p['orderId'] and int(r['store_id'])==tenant);return {'detail':order(row,o,module,lane,phase,True)}
 rec='user.recharge' in path;table='recharge_order' if rec else 'order';rows=[r for r in o[table] if int(r['store_id'])==tenant and (rec or int(r['is_delete'])==0)]
 if rec:
  if 'user_id' in p:rows=[r for r in rows if int(r['user_id'])==p['user_id']]
  if 'recharge_type' in p:rows=[r for r in rows if int(r['recharge_type'])==p['recharge_type']]
  if 'pay_status' in p:rows=[r for r in rows if int(r['pay_status'])==p['pay_status']]
  if 'search' in p:rows=[r for r in rows if p['search'] in r['order_no'] or any(p['search'] in u['nick_name'] for u in o['user'] if int(u['user_id'])==int(r['user_id']))]
  if 'betweenTime' in p:rows=[] # This frozen dataset has pay_time=0, outside the declared 2026 window.
 else:
  if p['dataType']=='pay':rows=[] # Existing own rows are cancelled20, never NORMAL10.
  if 'userId' in p:rows=[r for r in rows if int(r['user_id'])==p['userId']]
  if 'searchValue' in p:rows=[r for r in rows if p['searchValue'] in r['order_no']]
  if 'betweenTime' in p:rows=[r for r in rows if date(r['create_time']).startswith('2027-01-15')]
 rows.sort(key=lambda r:-int(r['create_time']));size=15 if rec else 10;total=len(rows);requested=p.get('page',1);current=max(1,min(requested,max(1,(total+size-1)//size)))
 page={'total':total,'per_page':size,'current_page':current,'last_page':(total+size-1)//size,'data':[(recharge(r,o,module,lane,phase) if rec else order(r,o,module,lane,phase,False)) for r in rows[(requested-1)*size:requested*size]]}
 return {'list':page} if rec else {'dataType':p['dataType'],'list':page}

def normalized(body,fixture):
 d=copy.deepcopy(body);d['trace_id']='VALIDATED_TRACE';maps={k:{} for k in ('order_id','user_id','goods_id','order_goods_id','image_id','file_id','avatar_id','order_plan_id')}
 for module,ids in fixture['owned'].items():
  for t,k in [('order','order_id'),('user','user_id'),('goods','goods_id'),('order_goods','order_goods_id'),('upload_file','file_id'),('recharge_order_plan','order_plan_id')]:maps[k][ids[t]]=module+'.'+t
  maps['order_id'][ids['recharge_order']]=module+'.recharge_order';maps['image_id'][ids['upload_file']]=module+'.upload_file';maps['avatar_id'][ids['upload_file']]=module+'.upload_file'
 def walk(x):
  if isinstance(x,dict):
   for k,v in x.items():
    if k in maps and type(v) is int and v in maps[k]:x[k]=maps[k][v]
    else:walk(v)
  elif isinstance(x,list):
   for v in x:walk(v)
 walk(d);return d

def verify():
 cases={};bodies={};norm={};out={};count=0;positive=0
 for lane in ('B','C'):
  d=ROOT/'private-evidence'/lane;fix=json.loads((d/'fixture-journal.json').read_text());lookup={c['scenario']:c for f in d.glob('cases-*.json') for c in json.loads(f.read_text())};records=[]
  for line in (d/'private-wire.jsonl').read_text().splitlines():
   w=json.loads(line);s=w['scenario'];phase=s.split('_')[2];c=lookup[s];b=json.loads(w['raw']);count+=1
   assert w['http_status']==200 and type(b['status']) is int and set(b)=={'status','message','data','trace_id'} and re.fullmatch('[0-9a-f]{13}',b['trace_id'])
   historical=phase=='NA' and ((lane=='C' and c['name'] not in ('AUTH','ORDER_CROSS_DETAIL')) or (lane=='B' and c['name']=='ORDER_DETAIL'))
   if historical:
    assert b['status']==500
    if c['name']=='ORDER_DETAIL':assert b['message']=='未找到该订单记录'
    else:assert '-2007' in b['message'] and '[user]' in b['message']
   else:
    assert b['status']==c['expected_status'],s
    if b['status']==200:equal(b['data'],expected_data(w,c,lane,phase),s+'.original_projection');positive+=1
    else:equal(b['data'],[],s+'.error_data')
    if phase=='FINAL':norm[(lane,s)]=normalized(b,fix)
   if phase=='FINAL':
    expected_sources={'app/common/database/DmBuilder.php':'58e1e79ce69881cc305c6fd33adec0e9a3d030ac15b4c00ae464cbaa4cce2b59'}
    for path,f in [('app/common/model/Order.php','commonOrderCandidate.php'),('app/store/model/Order.php','storeOrderCandidate.php'),('app/terminal/model/Order.php','terminalOrderCandidate.php'),('app/store/model/recharge/Order.php','storeRechargeOrderCandidate.php'),('app/terminal/model/recharge/Order.php','terminalRechargeOrderCandidate.php'),('app/common/service/OrderReadResponse.php','OrderReadResponseCandidate.php'),('app/store/controller/Order.php','storeOrderControllerCandidate.php'),('app/terminal/controller/Order.php','terminalOrderControllerCandidate.php'),('app/store/controller/user/Recharge.php','storeRechargeControllerCandidate.php'),('app/terminal/controller/user/Recharge.php','terminalRechargeControllerCandidate.php')]:expected_sources[path]=hashlib.sha256((ROOT/f).read_bytes()).hexdigest()
    equal(w['source_sha256'],expected_sources,s+'.actual_source_identity')
   equal(w['original'],fix['after'],s+'.all12_own_tables_unchanged')
   bodies[(lane,s)]=b;records.append({'scenario':s,'status':b['status'],'historical_red':historical,'original_projection_equal':not historical and b['status']==200,'raw_sha256':hashlib.sha256(w['raw'].encode()).hexdigest()})
  out[lane]=records
 pairs=[];unchanged=[]
 for module in ('STORE','TERMINAL'):
  for name in ('ORDER_ALL','ORDER_DETAIL','RECHARGE_ALL'):
   s=f'PILOT_OR_FINAL_{module}_{name}';equal(norm[('B',s)],norm[('C',s)],s+'.full_BC_body');pairs.append(s)
   old=f'PILOT_OR_GREEN_{module}_{name}';a=copy.deepcopy(bodies[('B',old)]);b=copy.deepcopy(bodies[('B',s)]);a['trace_id']=b['trace_id']='VALIDATED_TRACE';equal(a,b,s+'.B_DTO_unchanged');unchanged.append(s)
 return {'HTTP':count,'source_original_positive_projections':positive,'final_full_BC_pairs':pairs,'B_whole_body_unchanged':unchanged,'cases':out,'original_HTTP_reds':22}

if __name__=='__main__':
 r=verify();(ROOT/'comparison.json').write_text(json.dumps(r,ensure_ascii=False,indent=2)+'\n');print(json.dumps({k:v for k,v in r.items() if k!='cases'},ensure_ascii=False))
