from pathlib import Path
import json,shutil,gzip,hashlib,subprocess
from mysql_baseline import ROOT,q
STORE=Path('/Users/wiscourper/Documents/workspace/zhct/zhctproject/store')
SOURCE_HASH={'zhct':'434f2ea099e8fa8e397cbe8adae6a5c4a73542fd25c9a94e170316c7d123149f','zhct2':'0b04a89f8a54e0aa8749eb398eb95abbf1c8edc6b4c74e8a5b1cffcb48fbe3a6'}
for number,db in [(1,'zhct'),(2,'zhct2')]:
    out=ROOT/'private/releases'/('front'+str(number)+'-dm-e8de470f-20261024');out.mkdir(parents=True,mode=0o700,exist_ok=True)
    source=ROOT/'private'/('final-'+db);data=ROOT/'private'/('final-data-'+db);schema=ROOT/'private'/('final-schema-'+db)
    b=json.loads((source/'baseline.json').read_text());m=json.loads((data/'manifest.json').read_text());statements=json.loads((schema/'statements.json').read_text());mapping=json.loads((schema/'columns.json').read_text());ix=json.loads((schema/'indexes.json').read_text());meta=json.loads((ROOT/'private'/('metadata-'+db+'_stage.json')).read_text())
    assert m['rows']==b['rows'] and not m['deferred_tables'] and not m['partial_rehearsal_only']
    for directory in ['sql','lob']:shutil.copytree(data/directory,out/directory,dirs_exist_ok=True)
    for i in ix:
        src=sorted([x for x in meta['indexes'] if x['TABLE_NAME']==i['table'] and x['INDEX_NAME']==i['source_index']],key=lambda x:int(x['SEQ_IN_INDEX']))
        i['columns']=[{'name':x['COLUMN_NAME'],'prefix':int(x['SUB_PART']) if x['SUB_PART'] else None,'binary':next(c['COLLATION_NAME'] for c in mapping if c['TABLE_NAME']==i['table'] and c['COLUMN_NAME']==x['COLUMN_NAME']).endswith('_bin') if next(c['COLLATION_NAME'] for c in mapping if c['TABLE_NAME']==i['table'] and c['COLUMN_NAME']==x['COLUMN_NAME']) else False} for x in src]
    table_comments=dict(line.split('\t',1) for line in q("SELECT TABLE_NAME,HEX(TABLE_COMMENT) FROM information_schema.TABLES WHERE TABLE_SCHEMA='"+db+"_stage';"))
    for t in meta['tables']:
        text=bytes.fromhex(table_comments[t['TABLE_NAME']]).decode();t['TABLE_COMMENT']=text
        if text:statements.append({'kind':'comment','table':t['TABLE_NAME'],'sql':'COMMENT ON TABLE "'+t['TABLE_NAME'].upper()+'" IS '+"'"+text.replace("'","''")+"'"})
    (out/'sql/01_schema.sql').write_text('\n'.join(s['sql']+';' for s in statements if s['kind']=='table')+'\n')
    (out/'sql/03_indexes_constraints.sql').write_text('\n'.join(s['sql']+ ('\n/' if s['kind']=='identity_range' else ';') for s in statements if s['kind']!='table')+'\n')
    for name,obj in [('column-mapping.json',mapping),('index-mapping.json',ix),('source-metadata.json',meta),('statement-mapping.json',statements)]:
        (out/name).write_text(json.dumps(obj,ensure_ascii=False,indent=2))
    profiles=[]
    for c in mapping:
        if 'auto_increment' not in c['EXTRA']:continue
        ti=c['TABLE_NAME'];ci=next(i for i,x in enumerate(b['metadata'][ti]) if x['name']==c['COLUMN_NAME']);vals=[]
        with gzip.open(source/(ti+'.hex.jsonl.gz'),'rt') as f:
            for l in f:vals.append(int(bytes.fromhex(json.loads(l)[ci]).decode()))
        profiles.append({'table':ti,'column':c['COLUMN_NAME'],'rows':b['counts'][ti],'source_next':str(c['TARGET_IDENTITY_SEED']),'max_value':str(max(vals)) if vals else None,'min_value':str(min(vals)) if vals else None})
    (out/'identity-source-profile.json').write_text(json.dumps(profiles,indent=2))
    shutil.copy2(ROOT/'private'/('approved-normalization-'+db+'.json'),out/'approved-data-changes.json')
    v=out/'verification';v.mkdir(exist_ok=True);shutil.copytree(source,v/'source',dirs_exist_ok=True);shutil.copy2(data/'float-source.json',v/'float-source.json')
    for name in ['verify_front_values.py','front_php_acceptance.php','verify_front_metadata.cjs','export_dm_data.cjs']:
        if (ROOT/'scripts'/name).exists():shutil.copy2(ROOT/'scripts'/name,out/name)
    tools=ROOT/'scripts/release-tools'
    if tools.exists():
        for p in tools.iterdir():
            if p.is_file() and p.suffix in ('.py','.cjs'):shutil.copy2(p,out/p.name)
    shutil.copytree(ROOT.parent/'foodsafe2-mysql-dm-migration-20261023/private/dm-package/node_modules',out/'node_modules',dirs_exist_ok=True)
    tracked=['application/common.php','application/p/logic/Bill.php','application/cores/database/Dm.php','application/p/logic/Cater.php','application/model/Dish.php','application/model/Bill.php']
    new_files=['application/common/service/DishFixedFloatWrite.php','application/cores/database/builder/Dm.php','tests/Bill/ZhipanRestaurantCase.check.php','tests/Database/DishFixedFloatBuilderContract.check.php','tests/Database/DishFixedFloatSource.check.php','tests/Database/DishFixedFloatRead.check.php','tests/Database/fixtures/DishFixedFloatSource.json']
    patch=subprocess.check_output(['git','-C',str(STORE),'diff','--']+tracked)
    for f in new_files:
        result=subprocess.run(['git','-C',str(STORE),'diff','--no-index','--','/dev/null',f],stdout=subprocess.PIPE)
        assert result.returncode in (0,1)
        patch+=result.stdout
    (out/'required-code.patch').write_bytes(patch)
    runtime_files=tracked+new_files[:2]
    code_hashes={f:hashlib.sha256((STORE/f).read_bytes()).hexdigest() for f in runtime_files}
    (out/'REQUIRED_CODE.sha256').write_text(''.join(h+'  '+f+'\n' for f,h in sorted(code_hashes.items())))
    (out/'required-code-files.json').write_text(json.dumps(code_hashes,indent=2))
    (out/'CODE_REQUIREMENTS.md').write_text("""# 应用代码要求

数据库对应store develop_dameng提交e8de470fa45ad9eed4877db04921f999e5779a6a，加required-code.patch中的三类修复：禁用AI数据库时禁止回落主库；智盘读取餐厅字段大小写；四个菜品固定小数位字段在DM中的写入量化与物理读取显示。代码与回归测试已写入本地源码，尚未推送云端。不要在已经修改的本地代码上重复apply补丁。

由包含这些修复的提交运行云效流水线。部署PHP制品后，在对应源码根目录执行sha256sum -c <本包绝对路径>/REQUIRED_CODE.sha256，所有8个运行文件均通过，再进行页面验收。流水线以后有其它代码变更时，重新审查差异，不可仅用旧SQL包名称判定版本兼容。

仅四个明确字段的标准ORM写入受新量化规则约束：ydy_dish.food_weight；ydy_dishes.ssb/saccharides/price。复用原MySQL5.6非STRICT数值前缀转换，包括清空值转0。MySQL连接、DECIMAL/未知字段、已有Expression/INC/原始SQL不改。已验证3161个真实TP5编译断言、164个源数值观察及492个真实DM/PDO读写对照。不会将SUM或计算值作为物理FLOAT字段统一格式化。

当前主业务查询、权限和事务路径已列验收；不能据此宣称全部新模块、原始SQL表达式或全产品未来输入语义已验证。后厨hcgj独立库尚未提供，不创建或伪造其价格表、AI账户库。外设打印/支付/第三方HTTP和现场浏览器仍需后续验收。
""")
    tables=[]
    for t in m['tables']:
        auto=next((c for c in mapping if c['TABLE_NAME']==t['table'] and 'auto_increment' in c['EXTRA']),None)
        tables.append({'table':t['table'],'rows':t['rows'],'data_files':[t['data_file']] if t['data_file'] else [],'identity':bool(auto),'identity_seed':auto['TARGET_IDENTITY_SEED'] if auto else None})
    manifest={'dataset':'front'+str(number)+'-20261024','source_sha256':SOURCE_HASH[db],'code_commit':'e8de470fa45ad9eed4877db04921f999e5779a6a','code_patch_sha256':hashlib.sha256(patch).hexdigest(),'target_tables':284,'target_columns':4187,'total_rows':b['rows'],'tables':tables,'destination':{'app':'ZHCT_APP' if number==1 else 'ZHCT2_APP','probe':'ZHCT_PROBE' if number==1 else 'ZHCT2_PROBE','tablespace':'ZHCT_TBS' if number==1 else 'ZHCT2_TBS'},'max_source_field_utf8_bytes':max(n for values in b['max_bytes'].values() for n in values.values()),'files':{}}
    for p in sorted(out.rglob('*')):
        rel=str(p.relative_to(out))
        if not p.is_file() or rel in ('manifest.json','PAYLOAD.sha256') or rel.startswith(('private/','probe-','run-')):continue
        manifest['files'][rel]={'bytes':p.stat().st_size,'sha256':hashlib.sha256(p.read_bytes()).hexdigest()}
    (out/'manifest.json').write_text(json.dumps(manifest,indent=2))
    paths=[p for p in sorted(out.rglob('*')) if p.is_file() and p.name!='PAYLOAD.sha256' and not str(p.relative_to(out)).startswith(('private/','probe-','run-'))]
    (out/'PAYLOAD.sha256').write_text(''.join(hashlib.sha256(p.read_bytes()).hexdigest()+'  '+str(p.relative_to(out))+'\n' for p in paths))
    print(json.dumps({'package':str(out),'tables':284,'rows':b['rows'],'files':len(manifest['files']),'runtime_not_frozen_yet':True}))
