#!/usr/bin/env python3
"""Read a scoped WeCom snapshot and render locally; never modifies source records."""
import json,subprocess,hashlib,argparse,math
from pathlib import Path
from datetime import datetime,timedelta,timezone
from urllib.parse import urlparse
from build_dashboard import project,percent,BASE,URL
from sales_analysis import load_sales
DOC='s3_AHkAH3hlAHwCNUzm1hPTKQQGwNBft_a'
PROJECT_FIELDS=['项目编号','项目名称','项目阶段','项目状态','项目完成度','交付负责人','预期上线日期','开始时间','最近活动日期','本周关键成果','下一交付门槛','需要确认','第一阶段状态','第一阶段延期天数','第一阶段计划完成日','第一阶段实际完成日','第一阶段延期原因','第一阶段下一动作','第一阶段交付结论','验收状态','结束时间']
SERVICE_FIELDS=['项目名称','问题类型','接报时间','完成时间','问题描述','处理措施','售后人员']
def call(args):
 r=subprocess.run(['wecom-cli',*args],text=True,capture_output=True,timeout=60)
 x=json.loads(r.stdout)
 if r.returncode or x.get('errcode')!=0:raise RuntimeError(str(x.get('error',x.get('errmsg','CLI failed'))))
 x.pop('extra_identity_context',None);return x

def excel_date(v):
 if v is None or v=='':return ''
 if isinstance(v,(int,float)) and math.isfinite(v) and 1<=v<100000:
  return (datetime(1899,12,30)+timedelta(days=v)).date().isoformat()
 return ''

def safe_project_url(v):
 try:
  u=urlparse(v or '')
  return v if u.scheme=='https' and u.hostname=='doc.weixin.qq.com' and u.path.startswith(('/sheet/','/smartsheet/','/doc/','/smartpage/')) else ''
 except ValueError:return ''

def rows(payload):
 v=payload['values'][0];v=json.loads(v) if isinstance(v,str) else v
 return v['rows']

def normalize_project(r,i,anchor):
 q={k:('' if v is None else v) for k,v in r.items()};name=q['项目名称'];q['项目名称']=name.get('text','') if isinstance(name,dict) else str(name)
 for k in ['预期上线日期','开始时间','最近活动日期','结束时间']:q[k]=excel_date(q.get(k))
 people=[v.get('name','') for v in (r.get('交付负责人') or []) if v.get('name')]
 q['交付负责人']='、'.join(people);p=project(q,i,anchor);p['owners']=people;p['ownerText']='、'.join(people) or '未填写'
 p.update(sourceLink=safe_project_url(name.get('url','') if isinstance(name,dict) else ''),needs=r.get('需要确认') or '',achievement=r.get('本周关键成果') or '',phaseState=r.get('第一阶段状态') or '',phaseDelay=r.get('第一阶段延期天数'),phasePlan=excel_date(r.get('第一阶段计划完成日')),phaseActual=excel_date(r.get('第一阶段实际完成日')),phaseReason=r.get('第一阶段延期原因') or '',phaseNext=r.get('第一阶段下一动作') or '',phaseConclusion=r.get('第一阶段交付结论') or '')
 return p

def main(output):
 output.mkdir(parents=True,exist_ok=True);now=datetime.now(timezone(timedelta(hours=8)));stamp=now.isoformat(timespec='seconds');anchor=now.date().isoformat()
 meta=call(['smartsheet','sheets','list','--json',json.dumps({'docid':DOC})]);sheets={s['title']:s for s in meta['sheets']}
 extracts={}
 for title,fields in [('2-项目管理总表',PROJECT_FIELDS),('5-售后运维台账',SERVICE_FIELDS)]:
  expected=sheets[title]['record_count']
  if expected>200:raise RuntimeError('Scope exceeds reviewed 200-row extraction; keep last valid snapshot')
  sql='SELECT RECORD_ID, '+', '.join('`'+f+'`' for f in fields)+' FROM `'+title+'` LIMIT 200'
  extracts[title]=rows(call(['smartsheet','records','query','--docid',DOC,'--sql',sql]))
  if len(extracts[title])!=expected:raise RuntimeError('Incomplete extraction; keep last valid snapshot')
  if len({r['RECORD_ID'] for r in extracts[title]})!=expected:raise RuntimeError('Duplicate record identities')
 data=dict(connected=True,snapshotLabel='在线读取 · '+now.strftime('%m月%d日 %H:%M'),notice='数据读取于 '+now.strftime('%Y-%m-%d %H:%M')+'；这是本次只读快照，不会自动刷新。完成度空值为待填报；第一阶段“开始时间+15天”的公式信号不等于合同延期。',snapshotDate=anchor,fetchedAt=stamp,sourceUrl=URL+'?tab=q979lj&viewId=vh39yO',projects=[normalize_project(r,i,anchor) for i,r in enumerate(extracts['2-项目管理总表'],1)],service=[],sales=load_sales())
 for r in extracts['5-售后运维台账']:
  types=r.get('问题类型') or '';types='、'.join(types) if isinstance(types,list) else str(types)
  data['service'].append(dict(name=str(r.get('项目名称') or '未填写'),type=types,started=excel_date(r.get('接报时间')),finished=excel_date(r.get('完成时间')),description=r.get('问题描述') or '',action=r.get('处理措施') or '',owner='、'.join(v.get('name','') for v in (r.get('售后人员') or []))))
 raw=json.dumps(data,ensure_ascii=False).replace('<','\\u003c').replace('>','\\u003e').replace('&','\\u0026')
 html=(BASE/'dashboard-template.html').read_text().replace('__DATA__',raw)
 receipt={'fetched_at':stamp,'project_records':len(data['projects']),'service_records':len(data['service']),'progress_filled':sum(p['progress'] is not None for p in data['projects']),'project_link_count':sum(bool(p['sourceLink']) for p in data['projects']),'source_fields':PROJECT_FIELDS,'sha256':hashlib.sha256(html.encode()).hexdigest()}
 # Write only after complete validated extraction; private record values never enter Git.
 (output/'snapshot.json').write_text(json.dumps(data,ensure_ascii=False,indent=2));(output/'delivery-project-command-center.html').write_text(html);(output/'live-receipt.json').write_text(json.dumps(receipt,ensure_ascii=False,indent=2))
 print(json.dumps(receipt,ensure_ascii=False))
if __name__=='__main__':
 parser=argparse.ArgumentParser(description=__doc__);parser.add_argument('--output',type=Path,required=True);args=parser.parse_args();main(args.output)
