"""Copy native insertion parts and their full relationship graph into existing packages.
Existing body slide XML/media remain unchanged except for page number text.
No PowerPoint content is flattened or rebuilt during this package merge.
"""
from pathlib import Path
from zipfile import ZipFile,ZIP_DEFLATED
from lxml import etree as E
import json,posixpath,re,hashlib,copy
R=Path(__file__).parent;D=R/'deliverables';repo=R
while not (repo/'standards-stack').exists():repo=repo.parent
P='http://schemas.openxmlformats.org/presentationml/2006/main';A='http://schemas.openxmlformats.org/drawingml/2006/main';REL='http://schemas.openxmlformats.org/package/2006/relationships';RNS='http://schemas.openxmlformats.org/officeDocument/2006/relationships';CT='http://schemas.openxmlformats.org/package/2006/content-types'
ns={'p':P,'a':A,'r':RNS};xml=lambda x:E.fromstring(x);enc=lambda e:E.tostring(e,xml_declaration=True,encoding='UTF-8',standalone=True)
def relpath(part):return posixpath.join(posixpath.dirname(part),'_rels',posixpath.basename(part)+'.rels')
def resolve(part,target):return target.lstrip('/') if target.startswith('/') else posixpath.normpath(posixpath.join(posixpath.dirname(part),target))
def slideparts(data):
 rel={x.get('Id'):x.get('Target') for x in xml(data['ppt/_rels/presentation.xml.rels'])}
 return [resolve('ppt/presentation.xml',rel[x.get('{'+RNS+'}id')]) for x in xml(data['ppt/presentation.xml']).find('{'+P+'}sldIdLst')]
def add_disclosure(root):
 tree=root.find('p:cSld/p:spTree',ns);mx=max([int(x.get('id')) for x in tree.findall('.//p:cNvPr',ns)]+[0])+1
 sp=E.SubElement(tree,'{'+P+'}sp');nv=E.SubElement(sp,'{'+P+'}nvSpPr');E.SubElement(nv,'{'+P+'}cNvPr',id=str(mx),name='内部交流使用范围');E.SubElement(nv,'{'+P+'}cNvSpPr',txBox='1');E.SubElement(nv,'{'+P+'}nvPr');pr=E.SubElement(sp,'{'+P+'}spPr');tr=E.SubElement(pr,'{'+A+'}xfrm');E.SubElement(tr,'{'+A+'}off',x='650000',y='1670000');E.SubElement(tr,'{'+A+'}ext',cx='10800000',cy='200000');geom=E.SubElement(pr,'{'+A+'}prstGeom',prst='rect');E.SubElement(geom,'{'+A+'}avLst');E.SubElement(pr,'{'+A+'}noFill');tx=E.SubElement(sp,'{'+P+'}txBody');E.SubElement(tx,'{'+A+'}bodyPr',lIns='0',rIns='0',tIns='0',bIns='0');E.SubElement(tx,'{'+A+'}lstStyle');pa=E.SubElement(tx,'{'+A+'}p');run=E.SubElement(pa,'{'+A+'}r');rp=E.SubElement(run,'{'+A+'}rPr',lang='zh-CN',sz='900');fill=E.SubElement(rp,'{'+A+'}solidFill');E.SubElement(fill,'{'+A+'}srgbClr',val='68716C');E.SubElement(rp,'{'+A+'}latin',typeface='Microsoft YaHei');E.SubElement(rp,'{'+A+'}ea',typeface='Microsoft YaHei');E.SubElement(run,'{'+A+'}t').text='内部交流版｜案例材料仅供分享交流，不得用于公开宣传'
with ZipFile(D/'inserts-draft.pptx') as z:ins={n:z.read(n) for n in z.namelist()}
insertparts=slideparts(ins);manifest=json.loads((R/'insert-manifest.json').read_text());lookup={m['id']:i for i,m in enumerate(manifest)};out=[]
base5=repo/'modules/product/products/smart-canteen/product-solutions/2026-09-22-five-scene-solutions'
sources={'government':repo/'modules/product/products/smart-canteen/product-solutions/2026-09-22-government-scene-plan/ppt-v01/deliverables/政府机关智慧食堂一体化解决方案.pptx'}
for sc in ['enterprise','university','k12','hospital','eldercare']:
 data=json.loads((base5/sc/'slides.json').read_text());sources[sc]=base5/'deliverables'/sc/(data['name']+'.pptx')
for scene,base in sources.items():
 with ZipFile(base) as z:data={n:z.read(n) for n in z.namelist()}
 old=slideparts(data);base_cover=xml(data[old[0]]);number_template=next(sp for sp in base_cover.findall('.//p:sp',ns) if any(re.fullmatch(r'\d{2}\s*/\s*\d+',t.text or '') for t in sp.findall('.//a:t',ns)));root=xml(data['ppt/presentation.xml']);rels=xml(data['ppt/_rels/presentation.xml.rels']);types=xml(data['[Content_Types].xml']);itypes=xml(ins['[Content_Types].xml']);ctmap={x.get('PartName').lstrip('/'):x.get('ContentType') for x in itypes if x.tag.endswith('Override')};defaultmap={x.get('Extension'):x.get('ContentType') for x in itypes if x.tag.endswith('Default')}
 copied={};new_master_parts=[]
 def cp(part):
  if part in copied:return copied[part]
  match=re.fullmatch(r'ppt/slides/slide(\d+)\.xml',part)
  dest=f'ppt/slides/slide{1000+int(match.group(1))}.xml' if match else posixpath.join(posixpath.dirname(part),'insert-'+posixpath.basename(part))
  copied[part]=dest;data[dest]=ins[part]
  if part in ctmap:E.SubElement(types,'{'+CT+'}Override',PartName='/'+dest,ContentType=ctmap[part])
  else:
   ext=part.rsplit('.',1)[-1]
   if ext in defaultmap and not any(x.get('Extension')==ext for x in types):E.SubElement(types,'{'+CT+'}Default',Extension=ext,ContentType=defaultmap[ext])
  rp=relpath(part)
  if rp in ins:
   r=xml(ins[rp])
   for e in r:
    if e.get('TargetMode')=='External':continue
    child=resolve(part,e.get('Target'));target=cp(child);e.set('Target',posixpath.relpath(target,posixpath.dirname(dest)))
   data[relpath(dest)]=enc(r)
  if ctmap.get(part)=='application/vnd.openxmlformats-officedocument.presentationml.slideMaster+xml':new_master_parts.append(dest)
  return dest
 selected=[('old',0),('new',0),('new',1),('new',2)]+[('old',i) for i in range(4,len(old)-2)]
 cases=[i for i,m in enumerate(manifest) if m.get('scene')==scene]
 if not cases:cases=[lookup['eldercare-gap']]
 selected += [('new',i) for i in cases]
 slist=root.find('{'+P+'}sldIdLst');slist.clear();ridnum=20000;page_mapping=[]
 for n,(kind,i) in enumerate(selected,1):
  part=old[i] if kind=='old' else cp(insertparts[i]);rid='rIdInsert'+str(ridnum);ridnum+=1
  E.SubElement(rels,'{'+REL+'}Relationship',Id=rid,Type=RNS+'/slide',Target=posixpath.relpath(part,'ppt'));E.SubElement(slist,'{'+P+'}sldId',id=str(256+n),attrib={'{'+RNS+'}id':rid})
  if kind=='old':
   sroot=xml(data[part])
   for t in sroot.findall('.//a:t',ns):
    if t.text and re.fullmatch(r'\d{2}\s*/\s*\d+',t.text):t.text=f'{n:02d} / {len(selected)}'
   if i==0:add_disclosure(sroot)
   data[part]=enc(sroot)
  else:
   sroot=xml(data[part]);footer=copy.deepcopy(number_template);c=footer.find('p:nvSpPr/p:cNvPr',ns);c.set('id',str(90000+n));c.set('name','Revision page number');ts=footer.findall('.//a:t',ns);ts[0].text=f'{n:02d} / {len(selected)}'
   for t in ts[1:]:t.text=''
   sroot.find('p:cSld/p:spTree',ns).append(footer);data[part]=enc(sroot)
  page_mapping.append({'page':n,'kind':kind,'original_page':i+1 if kind=='old' else None,'insert_id':manifest[i]['id'] if kind=='new' else None,'part':part})
 masters=root.find('{'+P+'}sldMasterIdLst')
 if masters is None:masters=E.Element('{'+P+'}sldMasterIdLst');root.insert(0,masters)
 mid=max([int(x.get('id')) for x in masters]+[2147483647])+1
 for part in set(new_master_parts):
  rid='rIdInsertMaster'+str(mid);E.SubElement(rels,'{'+REL+'}Relationship',Id=rid,Type=RNS+'/slideMaster',Target=posixpath.relpath(part,'ppt'));E.SubElement(masters,'{'+P+'}sldMasterId',id=str(mid),attrib={'{'+RNS+'}id':rid});mid+=1
 # Remove superseded presentation-to-slide links. Old unused parts are retained harmlessly.
 active={e.get('{'+RNS+'}id') for e in slist}
 for rel in list(rels):
  if rel.get('Type')==RNS+'/slide' and rel.get('Id') not in active:rels.remove(rel)
 for ct in list(types):
  if ct.tag.endswith('Override') and ct.get('PartName','').lstrip('/') not in data:types.remove(ct)
 data['ppt/presentation.xml']=enc(root);data['ppt/_rels/presentation.xml.rels']=enc(rels);data['[Content_Types].xml']=enc(types)
 if 'docProps/app.xml' in data:
  ap=xml(data['docProps/app.xml'])
  for el in ap.iter():
   if E.QName(el).localname=='Slides':el.text=str(len(selected))
  data['docProps/app.xml']=enc(ap)
 destdir=D/'drafts'/scene;destdir.mkdir(parents=True,exist_ok=True);dest=destdir/(base.stem+'_V2.1_公司与案例版.pptx')
 with ZipFile(dest,'w',ZIP_DEFLATED) as z:
  for name,blob in data.items():z.writestr(name,blob)
 out.append({'scene':scene,'base':str(base),'draft':str(dest.resolve()),'name':dest.name,'pages':len(selected),'cases':len(cases) if scene!='eldercare' else 0,'page_mapping':page_mapping})
 print(scene,len(old),'->',len(selected),'cases',len(cases))
(R/'merge-manifest.json').write_text(json.dumps(out,ensure_ascii=False,indent=2))
