from pathlib import Path
import json,html,math,xml.etree.ElementTree as E
from PIL import ImageFont
R=Path(__file__).parent;D=R/'deliverables';D.mkdir(exist_ok=True)
data=json.loads((R/'architecture-data.json').read_text());F='/Users/jack/Library/Fonts/msyh.ttc';fontcache={}
O='#E65C1E';INK='#26332D';GRAY='#637269';BORDER='#D9E2DD';GREEN='#24775D';TEAL='#2E808B';AMBER='#AD6B0A';PURPLE='#76539C'
def font(n):
 if n not in fontcache:fontcache[n]=ImageFont.truetype(F,n)
 return fontcache[n]
def wrap(s,width,size):
 lines=[]
 for para in s.split('\n'):
  line=''
  for c in para:
   if line and font(size).getlength(line+c)>width:lines.append(line);line=c
   else:line+=c
  lines.append(line)
 return lines
class Diagram:
 def __init__(self,name,w):self.name=name;self.w=w;self.h=0;self.nodes=[];self.texts=[];self.edges=[];self.seq=0
 def text(self,t,x,y,w,size=24,color=INK,bold=False,line=1.42):
  ls=wrap(t,w,size);h=len(ls)*size*line;self.texts.append(dict(text=t,lines=ls,x=x,y=y,w=w,h=h,size=size,color=color,bold=bold,line=line));return h
 def box(self,id,x,y,w,h,fill='white',stroke=BORDER,r=10,dash=False,role='node'):
  self.nodes.append(dict(id=id,x=x,y=y,w=w,h=h,fill=fill,stroke=stroke,r=r,dash=dash,role=role))
 def title(self,t,sub):
  self.text(t,60,45,self.w-120,42,O,True);self.text(sub,62,112,self.w-124,23,GRAY)
 def label(self,n,t,y,sub=''):
  self.text(n,60,y,60,25,O,True);self.text(t,133,y,self.w-193,27,INK,True)
  if sub:self.text(sub,133,y+43,self.w-193,20,GRAY)
 def card(self,id,title,lines,x,y,w,h,color=O,fill='white',dash=False,fs=22):
  self.box(id,x,y,w,h,fill,color if dash else BORDER,10,dash)
  self.text(title,x+24,y+18,w-48,26,color,True)
  self.text('\n'.join(lines),x+24,y+61,w-48,fs,INK,line=1.5)
 def edge(self,id,x1,y1,x2,y2,source,target,color=O):self.edges.append(dict(id=id,x1=x1,y1=y1,x2=x2,y2=y2,source=source,target=target,color=color))
 def save(self,h):
  self.h=h;self.text('架构表达产品归属与业务分工；不代表全部功能、接口和数据已在任一项目集成上线。',62,h-78,self.w-124,21,GRAY)
  self.text('来源：智慧食堂产品知识库 2026-09-16 功能矩阵；命名与规划边界沿用本轮六场景口径。',62,h-43,self.w-124,17,GRAY)
  esc=html.escape;svg=[f'<svg xmlns="http://www.w3.org/2000/svg" width="{self.w}" height="{h}" viewBox="0 0 {self.w} {h}">',f'<title>{esc(self.name)}</title>','<desc>康比特智慧食堂产品功能架构。实框为资料列示；琥珀虚框为项目适配或接口；紫色虚框为规划。</desc>','<style>text {font-family: "Microsoft YaHei", "PingFang SC", sans-serif;}</style>','<defs><marker id="arrow" markerWidth="9" markerHeight="7" refX="8" refY="3.5" orient="auto"><path d="M0,0 L9,3.5 L0,7 Z" fill="#E65C1E"/></marker></defs>',f'<rect width="{self.w}" height="{h}" fill="#FCFDFB"/>']
  for a in self.nodes:
   svg.append(f'<rect id="{a["id"]}" data-graph-role="{a["role"]}" x="{a["x"]}" y="{a["y"]}" width="{a["w"]}" height="{a["h"]}" rx="{a["r"]}" fill="{a["fill"]}" stroke="{a["stroke"]}" stroke-width="1.5"'+(' stroke-dasharray="8 5"' if a['dash'] else '')+'/>')
  for a in self.edges:svg.append(f'<line id="{a["id"]}" data-graph-role="edge" data-source="{a["source"]}" data-target="{a["target"]}" x1="{a["x1"]}" y1="{a["y1"]}" x2="{a["x2"]}" y2="{a["y2"]}" stroke="{a["color"]}" stroke-width="2" marker-end="url(#arrow)"/>')
  for i,t in enumerate(self.texts):
   svg.append(f'<text id="text-{i}" x="{t["x"]}" y="{t["y"]+t["size"]}" fill="{t["color"]}" font-size="{t["size"]}" font-weight="{700 if t["bold"] else 400}">')
   for j,line in enumerate(t['lines']):svg.append(f'<tspan x="{t["x"]}" y="{t["y"]+t["size"]+j*t["size"]*t["line"]}">{esc(line)}</tspan>')
   svg.append('</text>')
  svg.append('</svg>');(D/(self.name+'.svg')).write_text('\n'.join(svg)+'\n')
  # All objects use their real geometry in Draw.io; text remains individually editable.
  model=E.Element('mxGraphModel',dx=str(self.w),dy=str(h),grid='1',gridSize='10',page='1',pageScale='1',pageWidth=str(self.w),pageHeight=str(h));root=E.SubElement(model,'root');E.SubElement(root,'mxCell',id='0');E.SubElement(root,'mxCell',id='1',parent='0')
  for a in self.nodes:
   style=f'rounded=1;arcSize=5;whiteSpace=wrap;html=1;fillColor={a["fill"]};strokeColor={a["stroke"]};strokeWidth=1.5;'+('dashed=1;' if a['dash'] else '')
   cell=E.SubElement(root,'mxCell',id=a['id'],value='',style=style,vertex='1',parent='1');E.SubElement(cell,'mxGeometry',x=str(a['x']),y=str(a['y']),width=str(a['w']),height=str(a['h']),attrib={'as':'geometry'})
  for i,t in enumerate(self.texts):
   val='<br>'.join(html.escape(x) for x in t['lines']);style=f'text;html=1;whiteSpace=wrap;overflow=visible;align=left;verticalAlign=top;spacing=0;fontFamily=Microsoft YaHei;fontSize={t["size"]};fontColor={t["color"]};fontStyle={1 if t["bold"] else 0};'
   cell=E.SubElement(root,'mxCell',id=f'text-{i}',value=val,style=style,vertex='1',parent='1');E.SubElement(cell,'mxGeometry',x=str(t['x']),y=str(t['y']),width=str(t['w']),height=str(t['h']+3),attrib={'as':'geometry'})
  for a in self.edges:
   cell=E.SubElement(root,'mxCell',id=a['id'],value='',style=f'endArrow=block;strokeColor={a["color"]};strokeWidth=2;html=1;',edge='1',parent='1');g=E.SubElement(cell,'mxGeometry',relative='1',attrib={'as':'geometry'});E.SubElement(g,'mxPoint',x=str(a['x1']),y=str(a['y1']),attrib={'as':'sourcePoint'});E.SubElement(g,'mxPoint',x=str(a['x2']),y=str(a['y2']),attrib={'as':'targetPoint'})
  return model
# Overview retains the agreed scene/software/terminal/service frame and opens modules below it.
W=2520;g=Diagram('01-产品功能架构全景图',W);g.title('康比特 AI 营养智慧食堂｜产品功能架构','L1 场景方案 → L2 软件产品 → L3 能力域 → L4 业务模块与关键动作；下方补齐终端、协作规则、接口与服务。')
g.text('读图：实框＝产品资料列示，需按版本核验　　琥珀虚框＝项目适配 / 第三方接口　　紫色虚框＝规划能力',64,166,W-128,21,GRAY)
g.label('01','六类场景方案与重点任务',222,'当前六份方案的场景划分；不是六套完全独立的软件，也不是公司全部一级行业分类。')
scenes=[('政府机关','身份餐标 · 福利补贴\n资金核验 · 食安责任'),('企业','班次供餐 · 档口商户\n交易应收 · 采购成本'),('高校','校园身份 · 校园卡\n高峰餐线 · 多食堂运营'),('K12','分学段食谱 · 学校审核\n采购经费 · 家长与监管'),('医院营养科〔适配〕','患者餐别 · 专业审核\n病区配送 · 摄入与退餐'),('养老机构〔适配〕','餐食适配 · 护理助餐\n实际摄入 · 家属授权')]
cw=(W-120-5*40)/6
for i,(t,b) in enumerate(scenes):g.card(f'scene-{i}',t,b.split('\n'),60+i*(cw+40),305,cw,150,color=AMBER if i>=4 else O,dash=i>=4,fs=21)
g.label('02','使用入口与岗位',498)
entries=[('管理 PC','后勤 / 财务 / 采购 / 食安 / 营养岗位'),('移动端与小程序','用餐者 / 现场执行 / 授权家属'),('供应商端','报价 / 接单 / 批次 / 对账'),('终端与大屏','现场采集 / 信息展示 / 授权公示')]
ew=(W-120-120)/4
for i,(t,b) in enumerate(entries):g.card(f'entry-{i}',t,[b],60+i*(ew+40),558,ew,115,color=GREEN,fs=20)
g.box('platform',60,719,W-120,92,fill=O,stroke=O);g.text('AI 营养智慧食堂平台｜三大系统组成产品组合',88,735,W-176,34,'white',True)
syw=(W-120-80)/3;syy=865;syh=1710
agg={
'nutrition':[
('人员与卡务','人员档案 / 部门与身份类别\n发卡 / 挂失冻结 / 补卡退卡'),
('账户与补贴','现金与补贴账户 / 发放与审核\n充值退款 / 冲正 / 流水核对'),
('消费规则','身份与档口策略 / 餐次时段\n次数与范围限制 / 规则生效'),
('订单与结算','消费订单 / 充值提现 / 退款\n异常补单 / 收入与应收统计'),
('菜品与菜单','配方与食物成分 / 菜谱维护\n营养标签 / 设备下发 / 菜牌'),
('预订与取餐','日期餐次 / 选餐支付 / 订单状态\n云打印与叫号 / 核销 / 评价'),
('营养与健康记录','个体与团体膳食分析 / 健康趋势\n饮食记录 / 阶段报告 / 体测记录'),
('AI 营养服务〔按版本〕','文字图片语音咨询 / 食物识别\n饮食打卡 / 图片解读 / 一般建议'),
('终端与运营配置','餐厅档口 / 设备与打印 / 人脸配置\nPad称位排菜 / 菜谱同步 / 权限')],
'procurement':[
('组织与基础资料','组织 / 仓库 / 角色权限\n商品分类 / 单位规格 / 业务规则'),
('供应商协同','档案资质 / 供货仓库 / 启停\n邀请绑定 / 接单 / 配送与标签'),
('价格与询比价','定价有效期 / 采购与定价询价\n在线报价 / 比价 / 调价审核'),
('申购与采购','需求提交 / 审批 / 申购转采购\n按供应商拆单 / 订单与履约'),
('验收与取证','有单无单 / 扣皮扣罚 / 净重\n照片视频 / 差异撤销 / 失败重传'),
('仓储与流转','验收入库 / 越库 / 领料出库\n调拨 / 批次追溯 / 安全库存'),
('退货与退库','仓库退货 / 厨房直退 / 批次价格\n领用退回 / 退货审核 / 单据关联'),
('盘点与账实核对','期初库存 / 盘点导入 / 秤端盘点\n盘盈盘亏 / 实盘与账存核对'),
('对账与经营分析','采购领料统计 / 收发存与月报\n供应商结算 / 价格成本 / 集团扩展')],
'safety':[
('分级管理与监管','食堂学校执行 / 区域监管\n授权下钻 / 督办抽查 / 统一规则'),
('资质与人员','食堂证照 / 健康证 / 供应商证照\n临期过期提示 / 状态与更新'),
('晨检与上岗记录','每日任务 / 设备采集 / 手工补录\n体温工装 / 异常报告 / 处置'),
('留样与消毒','菜品餐次批次 / 时间人员 / 图片\n智能留样记录 / 消毒登记与校验'),
('环境与快检','温湿度等监测 / 阈值与趋势\n农残快检 / 废弃物 / 设备检查'),
('视频与现场巡查','点位与分屏 / 实时查看 / 回放\nAI违规抓拍 / 事件时间与证据'),
('预警与责任处置','预警筛选 / 事件详情 / 核实处理\n责任岗位 / 状态 / 佐证上传'),
('整改与复核','检查任务 / 整改通知 / 期限\n复核通过或退回 / 督办与归档'),
('台账、追溯与公示','日常台账 / 票证批次 / 物资流向\n食安看板 / 授权公示 / 意见反馈')]}
for j,sys in enumerate(data['systems']):
 x=60+j*(syw+40);color=[O,GREEN,TEAL][j];g.box('sys-'+sys['key'],x,syy,syw,syh,fill='#F4F7F3',stroke=color,role='container');g.text(sys['name'],x+25,syy+19,syw-50,32,color,True);g.text(f"{sys['count']} 项功能 · 详图保留原入口与模块归属",x+25,syy+67,syw-50,21,GRAY)
 g.edge('owns-'+sys['key'],x+syw/2,811,x+syw/2,859,'platform','sys-'+sys['key'])
 for k,(t,b) in enumerate(agg[sys['key']]):g.card(f'{sys["key"]}-domain-{k}',t,b.split('\n'),x+22,syy+118+k*172,syw-44,132,color=color,fs=22)
g.label('03','智能终端｜按餐线和现场点位选配',2620,'数据由真实动作产生；设备数量、型号、接口与环境条件按项目配置。')
hw=[('就餐与结算','绑盘终端 / 膳识智能台\nAI识别台 / 消费机 / 档口收银'),('菜品与运营信息','电子菜牌 / 叫号与取餐\n营养大屏 / 后厨余量看板'),('采购与仓储','智能收货秤 / 双摄取证\n库存盘点机 / 扫码与打印'),('食品安全','晨检仪 / 留样秤与留样柜\n摄像头 / 环境监测 / 快检'),('健康服务〔选配〕','体测与营养查询等终端\n授权、数据类型与接口核验')]
hwW=(W-120-160)/5
for i,(t,b) in enumerate(hw):g.card(f'hardware-{i}',t,b.split('\n'),60+i*(hwW+40),2705,hwW,157,color=GREEN,fs=21)
g.label('04','协作规则与业务对象｜统一程度按项目核验',2906,'这是跨系统协作的设计要求；不能仅凭同属一个产品体系就声称已共用后台或数据库。')
common=[('组织与权限','单位 / 学校 / 食堂 / 档口 / 仓库\n角色、数据范围、审核与导出'),('数据对象与编码','人员 / 菜品配方 / 食材批次\n餐次 / 订单 / 票证 / 风险事件'),('责任与恢复','经办 / 审核 / 复核 / 操作日志\n异常补录 / 重传 / 备份与核对')]
for i,(t,b) in enumerate(common):g.card(f'common-{i}',t,b.split('\n'),60+i*(syw+40),2991,syw,150,color=GRAY,fs=22)
g.text('重叠能力先定主责：组织、库存、供应商等数据由哪个系统维护、记账与同步，需在项目中明确。',68,3160,W-136,21,GRAY)
g.label('05','适配、接口与规划能力分开表达',3220)
for i,(t,b,c) in enumerate([
('场景专属适配','医院：专业审核、患者餐别、病区交接\n养老：餐食适配、助餐记录、摄入复评\nK12：学校审核、家长关系、供餐核对',AMBER),
('第三方接口〔逐项确认〕','统一身份 / 人事 / 校园卡 / 支付财务\n医院HIS/EMR / 机构档案 / 健康设备\n文档、授权、字段、联调与异常恢复',AMBER),
('规划目标〔非上线承诺〕','库存＋预算＋预计人数联动餐单\n供餐端智能建议与前后厨Agent协同\n专业审核与业务验证后再进入交付',PURPLE)]):g.card(f'boundary-{i}',t,b.split('\n'),60+i*(syw+40),3280,syw,195,color=c,dash=True,fs=22)
g.label('06','交付与专业服务',3517)
g.box('services',60,3579,W-120,100,fill='#EDF5EE',stroke=GREEN);g.text('需求与现场调查 → 方案配置 → 部署联调 → 岗位培训 → 运维支持 → 营养评审与周期复盘',87,3602,W-174,27,GREEN,True)
models=[g.save(3800)];diags=[g]
# Source-exact detailed diagrams, grouped by actual entry and module.
rolelabel={'PC后台':'后勤、运营、财务、营养与管理员','手机端':'本人使用或授权协助','智能排菜Pad':'餐厅运营与现场排菜岗位','商家PC后台':'采购、验收、库管、财务及负责人','商家小程序':'采购、验收及现场执行岗位','供应商PC/小程序':'供应商及配送协同人员','智能验收秤':'收货、领料和盘点岗位','扩展能力':'政府或集团项目按范围选用','PC监管端':'获授权的监管与督办岗位','PC学校端':'学校、后勤、食安及现场执行岗位','大屏端':'管理驾驶舱或获授权公示','学校小程序':'后勤及食堂现场执行人员','公众H5':'获授权的用餐者、家长或公众'}
for no,sys in enumerate(data['systems'],2):
 dg=Diagram(f'{no:02d}-{sys["name"]}模块详图',2200);color={'nutrition':O,'procurement':GREEN,'safety':TEAL}[sys['key']]
 dg.title(sys['name']+'｜模块与功能详图',f"逐项展开 {sys['count']} 个功能，按 {len(sys['groups'])} 个“使用入口 × 模块”分组；保留产品资料的原入口名称。")
 dg.text('所有实框均为产品资料列示，实际可用范围须绑定版本与项目验证；入口名称不意味着该场景的所有专属流程已具备。',62,166,2076,21,GRAY)
 byentry={}
 for item in sys['groups']:byentry.setdefault(item['entry'],[]).append(item)
 y=237;counter=0
 for entry,groups in byentry.items():
  dg.box('entryband-'+str(counter),60,y,2080,70,fill=color,stroke=color,role='container');dg.text(entry,84,y+15,520,29,'white',True);dg.text(rolelabel.get(entry,''),650,y+20,1440,22,'white');y+=110
  for st in range(0,len(groups),4):
   row=groups[st:st+4];width=490
   heights=[max(116,74+sum(len(wrap('· '+a['function'],width-48,21))*31 for a in item['functions'])) for item in row];rh=max(heights)
   for j,(it,h) in enumerate(zip(row,heights)):
    x=60+j*530;counter+=1;dg.card(f'module-{counter}',it['name'],['· '+f['function'] for f in it['functions']],x,y,width,rh,color=color,fs=21)
   y+=rh+40
  y+=15
 notes={
 'nutrition':['业务输出：人员与账户关系、消费订单及流水、菜谱与取餐记录、营养分析和服务反馈。','边界：金额订单不能直接推算菜品营养；取餐量不等于真实摄入；库存预算自动出餐单另列规划。'],
 'procurement':['业务输出：供应商与定价依据、采购及实收单、批次库存、领用退货、盘点与对账记录。','边界：采购下单不等于入库；应收或对账不等于银行到账；集团扩展和自动餐单联动需逐项核验。'],
 'safety':['业务输出：资质与日常台账、现场巡查与告警、处置佐证、整改复核及获授权的公示信息。','边界：AI告警提供风险线索；最终核实、处置与复核由责任岗位完成。学校/监管入口用于其他场景须确认适配。']}
 dg.box('result',60,y,2080,150,fill='#F2F6F0',stroke=color);dg.text('\n'.join(notes[sys['key']]),84,y+24,2032,23,INK,line=1.55);y+=202
 models.append(dg.save(y+100));diags.append(dg)
mx=E.Element('mxfile',host='app.diagrams.net',type='device',version='24.7.17')
for i,(model,dg) in enumerate(zip(models,diags),1):d=E.SubElement(mx,'diagram',id='page-'+str(i),name=dg.name);d.append(model)
E.ElementTree(mx).write(D/'产品功能架构_四张可编辑图.drawio',encoding='utf-8',xml_declaration=True)
checks=[]
for dg in diags:
 for t in dg.texts:
  assert t['x']>=0 and t['y']>=0 and t['x']+t['w']<=dg.w and t['y']+t['h']<=dg.h,(dg.name,t)
  for line in t['lines']:assert font(t['size']).getlength(line)<=t['w']+1
 checks.append({'name':dg.name,'width':dg.w,'height':dg.h,'boxes':len(dg.nodes),'text_blocks':len(dg.texts),'edges':len(dg.edges)})
for sys,dg in zip(data['systems'],diags[1:]):
 blob='\n'.join(t['text'] for t in dg.texts)
 for group in sys['groups']:
  for f in group['functions']:assert f['function'] in blob,f
(R/'geometry-and-coverage.json').write_text(json.dumps({'diagrams':checks,'source_function_count':sum(s['count'] for s in data['systems']),'entry_module_groups':sum(len(s['groups']) for s in data['systems']),'all_source_functions_present':True,'text_geometry':'pass','visual_review':'pending'},ensure_ascii=False,indent=2))
print(json.dumps(checks,ensure_ascii=False));print('Source-exact functions:',sum(s['count'] for s in data['systems']))
