from pathlib import Path
import importlib.util, json, shutil, subprocess, sys, zipfile
from xml.sax.saxutils import escape
from lxml import etree as E
from reportlab.pdfgen import canvas
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
from reportlab.lib.colors import HexColor
from reportlab.lib.styles import ParagraphStyle
from reportlab.platypus import Paragraph

HERE=Path(__file__).resolve().parent
ROOT=HERE.parents[6]
OUT=ROOT/'oss/local-deliverables/2026-09-23-AI智慧健康站一页纸-v2-微软雅黑'
QA=OUT/'qa'
SKILL=ROOT/'standards-stack/agent-skills/skills/kangbite-word-template'
DATA=json.loads((HERE/'content.json').read_text())
spec=importlib.util.spec_from_file_location('v1',HERE.parent/'build.py')
v1=importlib.util.module_from_spec(spec);spec.loader.exec_module(v1)

FONT_DIR=Path('/Users/jack/Library/Fonts')
FONT_NAME='Microsoft YaHei'

def set_yahei(blob):
    root=E.fromstring(blob)
    for fonts in root.findall('.//w:rFonts',v1.N):
        fonts.attrib.clear()
        for key in ['ascii','hAnsi','eastAsia','cs']:
            fonts.set('{%s}%s' % (v1.W,key),FONT_NAME)
    for run in root.findall('.//w:r',v1.N):
        if run.find('w:t',v1.N) is None:continue
        rp=run.find('w:rPr',v1.N)
        if rp is None:rp=v1.el('rPr');run.insert(0,rp)
        fonts=rp.find('w:rFonts',v1.N)
        if fonts is None:
            fonts=v1.el('rFonts');rp.insert(0,fonts)
        for key in ['ascii','hAnsi','eastAsia','cs']:
            fonts.set('{%s}%s' % (v1.W,key),FONT_NAME)
    return E.tostring(root,xml_declaration=True,encoding='UTF-8',standalone=True)

def build_text():
    target=OUT/'01-纯文字版';target.mkdir(parents=True,exist_ok=True)
    shutil.copy2(SKILL/'references/template-spec.md',QA/'artifact.md')
    shutil.copy2(SKILL/'assets/kangbite-digital-sports-word-template.docx',QA/'reference.docx')
    subprocess.run([sys.executable,str(SKILL/'scripts/prepare_working_copy.py'),str(QA/'reference.docx'),str(QA/'starter.docx')],check=True)
    for d in DATA:
        md=f"# {d['title']}\n\n{d['subtitle']}\n\n**{d['tagline']}**\n\n{d['intro']}\n\n"
        for sec in d['sections']: md+=f"## {sec['title']}\n\n"+'\n\n'.join(sec['paragraphs'])+'\n\n'
        md+=d['closing']+'\n\n'+d['note']+'\n'
        (target/(d['file']+'_纯文字版.md')).write_text(md)
        (target/(d['file']+'_纯文字版.txt')).write_text(md.replace('## ','').replace('# ','').replace('**',''))
        (HERE/(d['file']+'_纯文字版.md')).write_text(md)
        with zipfile.ZipFile(QA/'starter.docx') as z: parts={n:z.read(n) for n in z.namelist()}
        doc=E.fromstring(parts['word/document.xml']);body=doc.find('w:body',v1.N)
        sect=body.find('w:sectPr',v1.N)
        for child in list(body):
            if child is not sect:body.remove(child)
        nodes=[v1.p(d['title'],36,True,'000000',100,90,'Title'),v1.p(d['subtitle'],23,True,'000000',after=70),v1.p(d['tagline'],22,True,'000000',after=110),v1.p(d['intro'])]
        for sec in d['sections']:
            nodes.append(v1.heading(sec['title']));nodes.extend(v1.p(t) for t in sec['paragraphs'])
        nodes.extend([v1.p(d['closing'],20,True,'000000',140,80),v1.p(d['note'],16,False,'606872',80,0)])
        for i,node in enumerate(nodes):body.insert(i,node)
        parts['word/document.xml']=E.tostring(doc,xml_declaration=True,encoding='UTF-8',standalone=True)
        changed={'word/document.xml','word/styles.xml'}
        changed.update(n for n in parts if n.startswith(('word/header','word/footer')) and n.endswith('.xml'))
        for n in changed:
            if n in parts:parts[n]=set_yahei(parts[n])
        dest=target/(d['file']+'_纯文字版.docx')
        with zipfile.ZipFile(dest,'w',zipfile.ZIP_DEFLATED) as z:
            for n,b in parts.items():z.writestr(n,b)
        with zipfile.ZipFile(QA/'reference.docx') as ref,zipfile.ZipFile(dest) as final:
            for n in ref.namelist():
                if n not in changed:assert ref.read(n)==final.read(n)

W,H=595.276,841.89
NAVY='#123453';BLUE='#185E97';TEAL='#087F8C';ORANGE='#EB6429';INK='#183349';MUTED='#5D7285';PALE='#F0F5F8';WHITE='#FFFFFF'
pdfmetrics.registerFont(TTFont('CPT',str(FONT_DIR/'msyh.ttc')))
pdfmetrics.registerFont(TTFont('CPTBold',str(FONT_DIR/'msyhbd.ttc')))
BOUNDS=[]

def color(h):return HexColor(h)
def rect(c,x,y,w,h,fill,r=0,stroke=None):
    c.setFillColor(color(fill));c.setStrokeColor(color(stroke or fill));c.setLineWidth(.6)
    c.roundRect(x,H-y-h,w,h,r,fill=1,stroke=bool(stroke))
def line(c,x1,y1,x2,y2,col=MUTED,width=1):
    c.setStrokeColor(color(col));c.setLineWidth(width);c.line(x1,H-y1,x2,H-y2)
def arrow(c,x1,y1,x2,y2,col=TEAL):
    line(c,x1,y1,x2,y2,col,1.3)
    import math
    a=math.atan2(y2-y1,x2-x1)
    for side in [-1,1]:line(c,x2,y2,x2-5*math.cos(a+side*.55),y2-5*math.sin(a+side*.55),col,1.3)
def text(c,s,x,y,w,size=10,col=INK,bold=False,leading=None,align=0):
    st=ParagraphStyle('p',fontName='CPTBold' if bold else 'CPT',fontSize=size,leading=leading or size*1.5,textColor=color(col),wordWrap='CJK',alignment=align)
    para=Paragraph(escape(s).replace('\n','<br/>'),st);_,height=para.wrap(w,H)
    assert x>=0 and x+w<=W+1 and y+height<=H-15,(s,y,height)
    para.drawOn(c,x,H-y-height);BOUNDS.append({'text':s,'x':x,'y':y,'width':w,'height':height})
    return height
def circle(c,x,y,r,fill):c.setFillColor(color(fill));c.circle(x,H-y,r,fill=1,stroke=0)

def brand(c,label,logo):
    c.setTitle('康比特AI智慧健康站 '+label);c.setAuthor('康比特')
    c.drawImage(str(logo),36,H-48,width=104,height=16.18,mask='auto')
    text(c,label,345,33,214,9,MUTED,align=2)
    line(c,36,61,559,61,'#D9E3EA',.7)
def footer(c,d,page):
    line(c,36,756,559,756,'#D9E3EA',.7)
    text(c,d['note'],36,769,492,7.6,MUTED,leading=11.5)
    text(c,page,537,788,22,8,MUTED,align=2)
def section(c,num,title,y):
    text(c,num,36,y,26,9,ORANGE,True)
    text(c,title,67,y-3,470,13,NAVY,True)
def numbered_step(c,n,title,body,x,y,w):
    circle(c,x+11,y+11,11,TEAL)
    text(c,n,x+1,y+4,w=20,size=9,col=WHITE,bold=True,align=1)
    text(c,title,x,y+31,w,10,INK,True)
    text(c,body,x,y+50,w,8.5,MUTED,leading=12.5)

def product(c,d,logo):
    brand(c,'产品介绍  /  能力示意',logo)
    text(c,d['title'],36,79,523,26,NAVY,True,leading=33)
    text(c,d['subtitle'],36,120,523,12,BLUE,True)
    text(c,d['tagline'],36,151,523,16,INK,True)
    text(c,'以AI Agent为核心，将专业经验和个人数据转化为吃动建议，\n通过执行反馈与复测，让方案跟着身体状态变化。',36,180,523,10,MUTED,leading=15)
    rect(c,36,231,523,76,NAVY,10)
    text(c,'25年',52,243,95,27,WHITE,True)
    text(c,'运动营养经验积累',52,280,120,9,'#CBDCE9')
    line(c,202,248,202,290,'#527187',.7)
    text(c,'100支',220,243,105,27,WHITE,True)
    text(c,'国家队服务经验',220,280,123,9,'#CBDCE9')
    line(c,376,248,376,290,'#527187',.7)
    text(c,'营养垂类模型',394,247,150,16,WHITE,True)
    text(c,'以专业经验训练AI',394,280,150,9,'#CBDCE9')
    section(c,'01','多源数据进入AI  生成个人吃动方案',329)
    for y,title,body in [(359,'现场体测','身高 · 体重\n血压 · 体成分'),(419,'日常运动健康','百款硬件及应用生态\n华为运动健康 · 苹果健康'),(479,'个人记录','目标与饮食信息\n日常执行反馈')]:
        rect(c,36,y,159,52,PALE,6)
        text(c,title,48,y+6,135,10,NAVY,True)
        text(c,body,48,y+22,139,8.2,MUTED,leading=11)
        arrow(c,198,y+26,225,445)
    rect(c,232,399,149,95,TEAL,12)
    text(c,'AI Agent',245,410,123,20,WHITE,True,align=1)
    text(c,'AI运动营养师',245,444,123,11,WHITE,True,align=1)
    text(c,'分析数据  ·  形成建议',242,467,129,8.5,'#D6F2F2',align=1)
    for y,title,body in [(399,'运动方案','日常运动安排'),(454,'营养方案','三餐与按需补充建议')]:
        arrow(c,386,445,413,y+20)
        rect(c,420,y,139,42,'#FFF3EA',6)
        text(c,title,431,y+5,117,10,INK,True)
        text(c,body,431,y+23,117,8.2,MUTED)
    text(c,'按用户授权及接口能力接入；摄入信息由支持的数据来源与个人记录补充。',36,539,523,8,MUTED)
    section(c,'02','从检测到复测  形成持续服务闭环',568)
    steps=[('检测','获取记录'),('评估','理解状态'),('吃动','执行方案'),('反馈','记录感受'),('复测','观察变化'),('调整','更新建议')]
    for i,(t,b) in enumerate(steps):
        x=36+i*89
        circle(c,x+14,609,13,TEAL)
        text(c,str(i+1),x+4,601,20,10,WHITE,True,align=1)
        text(c,t,x+35,601,49,10,INK,True)
        text(c,b,x,630,78,8.5,MUTED)
        if i<5:arrow(c,x+70,609,x+83,609,'#97AEBE')
    line(c,492,650,492,660,TEAL);line(c,492,660,49,660,TEAL);arrow(c,49,660,49,650,TEAL)
    text(c,'一体机检测建档  +  AI小程序持续服务  +  营养品按需选配',36,683,523,11,NAVY,True)
    text(c,'适用于健身与健康管理机构、社区及企事业单位。\n从一次检测开始，体验报告解读、吃动建议与复测反馈。',36,710,523,9.2,MUTED,leading=14)
    footer(c,d,'01')

def channel(c,d,logo):
    brand(c,'渠道产品介绍  /  合作路径示意',logo)
    text(c,'把AI运动营养服务',36,80,523,26,NAVY,True,leading=33)
    text(c,'加入你的客户方案',36,116,523,26,NAVY,True,leading=33)
    text(c,'康比特AI智慧健康站｜一体机 + AI运动营养师 + 可选营养品',36,161,523,10.6,BLUE,True)
    text(c,'25年、100支国家队经验训练营养垂类模型；通过百款硬件及华为、苹果\n健康生态连接数据。渠道提供客户与行业经验，共同匹配具体服务场景。',36,187,523,9.5,MUTED,leading=14)
    section(c,'01','两类收入  收益来自真实成交与复购',235)
    for x,num,title,body in [(36,'01','设备差价','设备成交价与供货价之间形成差价，\n收益核算需扣除获客、交付和售后成本。'),(305,'02','营养品收益','依据真实订单和复购，按约定差价或\n分润结算，计入退款及履约成本。')]:
        rect(c,x,263,254,103,NAVY if num=='01' else TEAL,8)
        text(c,num,x+15,275,26,10,'#B9D6E5',True)
        text(c,title,x+51,271,180,18,WHITE,True)
        text(c,body,x+15,309,225,9,WHITE,leading=15)
    section(c,'02','把产品接到渠道已有的客户关系中',388)
    for x,title,body in [(36,'渠道伙伴','客户资源与行业经验'),(224,'门店 / 企业','组织检测与持续服务'),(412,'终端用户','看懂身体  执行吃动建议')]:
        rect(c,x,419,147,59,PALE,7)
        text(c,title,x+10,428,127,13,NAVY,True,align=1)
        text(c,body,x+6,454,135,8.4,MUTED,align=1)
    arrow(c,190,448,217,448);arrow(c,378,448,405,448)
    text(c,'检测建立联系 → AI提供日常服务内容 → 复测创造回访机会 → 按需配置营养品',36,490,523,9.3,TEAL,True)
    section(c,'03','两类客户资源  各自匹配真实需求',526)
    line(c,36,554,559,554,'#D9E3EA',.7)
    text(c,'门店与机构',36,567,114,12,NAVY,True)
    text(c,'健身 / 瑜伽 / 健康管理 / 体检 / 美容养生\n结合现有服务补充检测、营养建议与复测回访。',165,565,394,9.5,MUTED,leading=15)
    line(c,36,603,559,603,'#D9E3EA',.7)
    text(c,'企业与平台',36,616,114,12,NAVY,True)
    text(c,'员工福利服务商 / 企业HR / 行政 / 工会\n匹配员工健康服务；具体需求与合作方式共同确认。',165,614,394,9.5,MUTED,leading=15)
    text(c,'从一个真实点位开始',36,666,523,13,NAVY,True)
    text(c,'提供场景  →  产品演示  →  确认分工与商务  →  点位验证  →  评估复制',36,695,523,10,TEAL,True)
    text(c,'设备与软件可打包；赠送范围、期限、续费、培训及交付支持以正式约定为准。',36,724,523,8.3,MUTED)
    footer(c,d,'02')

def main():
    QA.mkdir(parents=True,exist_ok=True);build_text()
    with zipfile.ZipFile(QA/'reference.docx') as z:(QA/'cpt-logo.png').write_bytes(z.read('word/media/image1.png'))
    target=OUT/'02-图文版';target.mkdir(exist_ok=True)
    for d,draw in zip(DATA,[product,channel]):
        c=canvas.Canvas(str(target/(d['file']+'_图文版.pdf')),pagesize=(W,H),pageCompression=1)
        draw(c,d,QA/'cpt-logo.png');c.showPage();c.save()
    (QA/'visual-text-bounds.json').write_text(json.dumps(BOUNDS,ensure_ascii=False,indent=2))
    print(OUT)

if __name__=='__main__':main()
