"""Independent full DTO projections from original physical rows and pinned source rules."""
from datetime import datetime,timezone,timedelta
import json,math
TZ=timezone(timedelta(hours=8))
def phpjson(v):
 if type(v) is float:return int(v) if v.is_integer() else v
 if type(v) is list:return [phpjson(x) for x in v]
 if type(v) is dict:return {k:phpjson(x) for k,x in v.items()}
 return v
def empty(v):return v is None or v is False or v==0 or v=='' or v=='0' or v==[]
def dt(v):return datetime.fromisoformat(str(v)).replace(tzinfo=TZ)
def modelrow(row,columns,hidden=(),timestamps=True):
 out={}
 for c in columns:
  k=c['column_name'].lower()
  if k in hidden or k not in row:continue
  v=row[k]
  if timestamps and k in ['create_time','update_time'] and v is not None:
   if isinstance(v,(int,float)) or str(v).isdigit():v=datetime.fromtimestamp(int(v),TZ).strftime('%Y-%m-%d %H:%M:%S')
   else:v=dt(v).strftime('%Y-%m-%d %H:%M:%S')
  out[k]=v
 return phpjson(out)
def pack(data,session):
 sid=int(session['session_id']);raws=[r for r in data['measurement_session_raw'] if int(r['session_id'])==sid];raws.sort(key=lambda r:int(r['id']),reverse=True)
 rows=[r for r in data['health_record'] if int(r['session_id'])==sid];rows.sort(key=lambda r:int(r['record_id']))
 summary={};segments={};av=None;aver=None
 for r in rows:
  code=str(r['metric_code']);item={'value':float(r['value']),'unit':r.get('unit')};seg=r.get('segment');av=av if av is not None else r.get('algorithm_vendor');aver=aver if aver is not None else r.get('algorithm_version')
  if empty(seg):summary[code]=item
  else:segments.setdefault(str(seg),{})[code]=item
 raw=None
 if raws and 'data' in raws[0]:
  try:raw=json.loads(str(raws[0]['data']))
  except (ValueError,TypeError):pass
 out={'session_id':sid,'measured_at':session.get('start_time'),'vendor':session.get('vendor','welland'),'device_type':session.get('device_type','body_scale'),'algorithm_vendor':av,'algorithm_version':aver,'summary':summary or [],'segments':segments or [],'raw':raw}
 b=summary.get('bmi',{}).get('value');out['bmi_cn']='' if b is None else '消瘦' if b<18.5 else '标准' if b<24 else '超重' if b<28 else '肥胖'
 return phpjson(out)
def expected(pre,cases):
 d=pre['source_data'];cols=pre['columns'];out={};actor=pre['actor']
 for label,c in cases.items():
  message='success';http=200;status=200;classification='full-original-row-projection'
  if label=='POINTS':
   rows=d['user_points_log'];items=[modelrow(r,cols['AI']['user_points_log'],['store_id']) for r in rows[:15]];result={'list':{'total':len(rows),'per_page':15,'current_page':1,'last_page':math.ceil(len(rows)/15),'data':items}};classification='nonempty' if rows else 'empty-only'
  elif label=='REFUNDS':
   if d['order_refund']:raise ValueError('Nonempty refund eager relations need exact source projection before HTTP; skip only this route')
   result={'list':{'total':0,'per_page':15,'current_page':1,'last_page':0,'data':[]}};classification='empty-only'
  elif label=='SIGN':
   action=int(c['params']['signId']);rows=[r for r in d['sign'] if int(r['action_id'])==action and int(r['is_delete'])==0]
   if len({r['address_id'] for r in rows})!=len(rows):raise ValueError('Sign group address_id source ambiguous duplicate; do not invent earliest row')
   items=[modelrow(r,cols['AI']['sign'],['store_id','is_delete','create_by','create_time','update_time']) for r in rows]
   users=[r for r in d['sign_user'] if int(r['action_id'])==action]
   info=modelrow(users[0],cols['AI']['sign_user'],['is_delete','create_by','create_time','update_time']) if users else {'id':0,'user_id':actor,'action_id':action,'status':1,'award_time':'','receive_time':'','address_id':0,'address':''}
   result={'list':items,'signInfo':info};classification='nonempty' if rows or users else 'empty-list/source-default-signInfo'
  elif label=='WEIGHT':
   end=int(datetime.fromisoformat(c['params']['date']).replace(tzinfo=TZ).timestamp());rows=[r for r in d['user_weight'] if int(r['date'])<=end][:7]
   result=[dict({k:r[k] for k in ['date','height','weight','bmi']},dateFmt=datetime.fromtimestamp(int(r['date']),TZ).strftime('%Y-%m-%d')) for r in rows];result=phpjson(result);classification='nonempty' if rows else 'empty-only'
  elif label.startswith(('WELL_','YKB_')):
   rows=[r for r in d['measurement_session'] if int(r['is_valid'])==1 and r['device_type']=='body_scale' and (not label.startswith('YKB_') or r['vendor']=='yunkangbao')]
   if label.endswith('DATES'):
    dates=sorted({str(r['start_time'])[:10] for r in rows},reverse=True);result={'list':dates or [pre['date_now']]};classification='nonempty-date-source' if dates else 'empty-source/current-day-fallback'
   else:
    if label.endswith('DATE'):
     start=datetime.fromisoformat(c['params']['date']+' 00:00:00').replace(tzinfo=TZ);end=start+timedelta(hours=23,minutes=59,seconds=59);rows=[r for r in rows if start<=dt(r['start_time'])<=end]
    rows.sort(key=lambda r:(dt(r['start_time']),int(r['session_id'])),reverse=True);result=pack(d,rows[0]) if rows else [];message='ok' if rows else '暂无数据';classification='nonempty' if rows else 'empty-only'
  elif label=='INDEX':result=[];status=404;http=404;message='接口不存在';classification='literal404-no-businessDB'
  else:raise ValueError(label)
  out[label]={'http_status':http,'status':status,'message':message,'data':result,'classification':classification}
 return out
