#!/usr/bin/env python3
"""Create a portable kit from this tool's isolated successful build. No deployment."""
from pathlib import Path
import argparse,hashlib,json,os,re,shutil,subprocess,tarfile

def sha(p):
 h=hashlib.sha256()
 with p.open('rb') as f:
  for b in iter(lambda:f.read(1024*1024),b''):h.update(b)
 return h.hexdigest()

def assemble(job,template,output):
 job=job.resolve();src=job/'src'
 if not (job/'.foodsafe-build-workspace').is_file():raise RuntimeError('缺独立打包目录标记')
 meta=json.loads((job/'build-meta.json').read_text());version=meta['version']
 if not re.fullmatch(r'[A-Za-z0-9][A-Za-z0-9._-]{0,70}',version):raise RuntimeError('版本号非法')
 name='foodsafe-'+version+'-kylin-x64';output.mkdir(parents=True,exist_ok=True)
 kit=output/name;archive=output/(name+'.tar.gz')
 if kit.exists() or archive.exists():raise RuntimeError('制品已存在，不能覆盖，换新版本号')
 # Validate necessary results before creating the kit.
 for rel in ['services/gateway-service/dist/services/gateway-service/src/main.js']+['apps/'+a+'/dist/index.html' for a in ['web-school','web-regulator','screen','mobile-device']]:
  if not (src/rel).is_file():raise RuntimeError('缺构建结果: '+rel)
 shutil.copytree(template,kit,symlinks=True);payload=kit/'payload';payload.mkdir()
 for rel in ['node_modules','services/gateway-service/node_modules','services/gateway-service/dist','libs']:
  target=payload/rel;target.parent.mkdir(parents=True,exist_ok=True)
  shutil.copytree(src/rel,target,symlinks=True)
 service=payload/'services/gateway-service';d=json.loads((src/'services/gateway-service/package.json').read_text());d['dependencies']['qrcode']='1.5.4'
 (service/'package.json').write_text(json.dumps(d,indent=2)+'\n')
 q=payload/'node_modules/qrcode'
 if not q.is_symlink():raise RuntimeError('期望打包临时qrcode链接，源码依赖结构已变化')
 q.unlink()
 # npm ci resolves exactly the supplied lock; keep its complete dependency closure.
 subprocess.run(['cp','-a',str(job/'extra/node_modules')+'/.',str(payload/'node_modules')+'/'],check=True)
 for app in ['web-school','web-regulator','screen','mobile-device']:
  target=payload/'apps'/app/'dist';target.parent.mkdir(parents=True,exist_ok=True)
  shutil.copytree(src/'apps'/app/'dist',target,symlinks=True)
  lib=target/'libs';lib.mkdir(exist_ok=True)
  for mod,name in [('hls.js','hls.min.js'),('flv.js','flv.min.js')]:
   resolved=subprocess.check_output(['node','-e',f'console.log(require.resolve("{mod}/dist/{name}"))'],cwd=src/'apps/web-school',text=True).strip()
   shutil.copyfile(resolved,lib/name)
  (target/'runtime-config.js').write_text('window.FOODSAFE_RUNTIME={};window.SCHOOL_INTEGRATION_BASE="/integration";window.DEWO_MQTT_HOST="/api";\n')
 # Build commands and two unused workspace links are not runtime dependencies.
 for p in list(payload.rglob('.bin')):
  if p.is_symlink():p.unlink()
  elif p.is_dir():shutil.rmtree(p)
 for root,dirs,files in os.walk(payload,followlinks=False):
  for name in files:
   p=Path(root)/name
   if name.startswith('.env') or name in ['.npmrc','.pnpmfile.cjs','.DS_Store','.modules.yaml']:
    p.unlink()
  for name in dirs+files:
   p=Path(root)/name
   if not p.is_symlink():continue
   rel=str(p.relative_to(payload))
   if not p.exists() and rel in ['node_modules/.pnpm/node_modules/stream-tester','node_modules/.pnpm/node_modules/@foodsafe/mobile-emergency']:
    p.unlink();continue
   if os.path.isabs(os.readlink(p)) or not p.exists():raise RuntimeError('绝对/失效链接: '+rel)
   if payload.resolve() not in p.resolve().parents:raise RuntimeError('链接越出制品: '+rel)
 subprocess.run(['chmod','-R','a+rX',str(payload)],check=True)
 # Load real driver and critical runtime modules inside the copied payload.
 subprocess.run(['node','-e',"const p=require('path'),{createRequire}=require('module');const r=createRequire(p.resolve('services/gateway-service/package.json'));for(const m of ['dmdb','qrcode','dotenv','@nestjs/core','express','mysql2','mqtt']){r(m);console.log(m+' OK')}"],cwd=payload,check=True)
 prov=kit/'provenance';prov.mkdir()
 shutil.copyfile(src/'pnpm-lock.yaml',prov/'source-pnpm-lock.yaml')
 shutil.copyfile(job/'extra/package-lock.json',prov/'qrcode-package-lock.json')
 shutil.copyfile(job/'logs/release-overlay.json',prov/'release-overlay.json')
 meta.update({'dmdb':d['dependencies']['dmdb'],'qrcode':'1.5.4','entry':'services/gateway-service/dist/services/gateway-service/src/main.js','cwd':'project root','database_migration_not_executed':True})
 (kit/'RELEASE.json').write_text(json.dumps(meta,indent=2)+'\n')
 # Include only a candidate MySQL script; never execute it or treat it as DM SQL.
 sql=src/'db/upgrade/zywxb.sql'
 if sql.exists():
  (kit/'sql-review').mkdir();shutil.copyfile(sql,kit/'sql-review/zywxb-mysql8-candidate.sql')
  (kit/'sql-review/README.txt').write_text('MySQL8历史升级候选，需独立审核；禁止直接在达梦执行。')
 manifest=[]
 for p in sorted(payload.rglob('*')):
  if p.is_file() and not p.is_symlink():manifest.append(sha(p)+'  '+str(p.relative_to(kit)))
 (kit/'PAYLOAD.sha256').write_text('\n'.join(manifest)+'\n')
 # symlink targets are validated above and included separately for auditing.
 links={str(p.relative_to(kit)):os.readlink(p) for p in payload.rglob('*') if p.is_symlink()}
 (kit/'SYMLINKS.json').write_text(json.dumps(links,indent=2)+'\n')
 with tarfile.open(archive,'w:gz',dereference=False) as tf:tf.add(kit,arcname=name)
 (output/(name+'.tar.gz.sha256')).write_text(sha(archive)+'  '+archive.name+'\n')
 print('打包完成（尚未部署）:',archive)
 print('下一步：sha256sum -c',archive.name+'.sha256，然后在隔离环境验收')
 return archive

if __name__=='__main__':
 p=argparse.ArgumentParser();p.add_argument('job',type=Path);p.add_argument('template',type=Path);p.add_argument('output',type=Path);a=p.parse_args()
 assemble(a.job,a.template,a.output)
