"""Publish one assembled review batch under the shared host lock, preserving other routes."""
from pathlib import Path
import argparse,fcntl,hashlib,importlib.util,json,shutil,datetime,os,requests,concurrent.futures,subprocess,re
root=Path(__file__).resolve().parents[1]
repo=root.parents[1]
parser=argparse.ArgumentParser();parser.add_argument('--journey',action='store_true');parser.add_argument('--legacy',type=Path);parser.add_argument('--contract',type=Path);parser.add_argument('--mirror',type=Path,help='Verified isolated full-host snapshot when the canonical mirror has an external preservation race');parser.add_argument('--deploy-timeout',type=int,default=1200,help='Task-local upload allowance. Does not change the canonical publishing skill.');args=parser.parse_args()
spec=importlib.util.spec_from_file_location('host_publisher','/Users/agency/.agents/skills/comment-html-review-host/scripts/publish_host.py');host=importlib.util.module_from_spec(spec);spec.loader.exec_module(host);core=host.core()
if args.mirror:
 host.SITE_ROOT=args.mirror.resolve()
 assert host.SITE_ROOT.is_dir(),'The verified snapshot directory is absent'
access=json.loads((root/'evidence/review-host-access.json').read_text());target=next(r for r in access if r.get('success') and r.get('project')=='cf-review' and 'review.clientsflow.hu' in r.get('domains',[]))
original_env=core.cloudflare_env
core.cloudflare_env=lambda:dict(original_env(),CLOUDFLARE_ACCOUNT_ID=target['account_id'])
def task_deploy(directory,project,branch):
 command=['npx','--yes','wrangler','pages','deploy',str(directory),'--project-name',project,'--branch',branch,'--commit-dirty=true']
 result=subprocess.run(command,env=core.cloudflare_env(),capture_output=True,text=True,timeout=args.deploy_timeout)
 output=result.stdout+'\n'+result.stderr
 if result.returncode:raise RuntimeError(output.strip())
 urls=re.findall(r'https://[^\s]+\.pages\.dev',output)
 assert urls,'Successful CLI exit did not identify a deployment. Verify provider state before retrying.'
 print(output.strip(),flush=True)
 return urls[-1]
core.deploy=task_deploy
timestamp=datetime.datetime.now(datetime.timezone.utc).strftime('%Y%m%dT%H%M%SZ')
changed=set();jobs=[]
if args.journey:jobs.append((root/'site/index.html','komplex-journey-20261002-master','komplex-journey-2026-10-02-v1',root/'site'))
if args.legacy:jobs.append((args.legacy/'index.html','komplex-rita-funnel-20260926-master','komplex-rita-funnel-2026-09-26-v1',args.legacy))
if args.contract:jobs.append((args.contract,'komplex-journey-contract-20261001','komplex-journey-contract-2026-10-01-v1',None))
assert jobs,'Specify at least one assembled batch'
changed={j[2] for j in jobs}
def digests():
 result={}
 for p in host.SITE_ROOT.rglob('*'):
  if not p.is_file():continue
  rel=p.relative_to(host.SITE_ROOT)
  if rel.parts[0] in changed or str(rel) in ['index.html','review-manifest.json','_worker.js','_routes.json']:continue
  result[str(rel)]=hashlib.sha256(p.read_bytes()).hexdigest()
 return result
def fresh_live_preservation():
 headers={'User-Agent':'Mozilla/5.0 Komplex Review Preservation'}
 response=requests.get('https://review.clientsflow.hu/review-manifest.json',headers=headers,timeout=30);response.raise_for_status()
 pages=response.json()['pages'];missing=[slug for slug in pages if not(host.SITE_ROOT/slug/'index.html').is_file()]
 assert not missing,f'New live pages require recovery before publishing: {missing}'
 def compare(slug):
  path=slug+'/index.html';r=requests.get('https://review.clientsflow.hu/'+slug+'/',headers=headers,timeout=35);r.raise_for_status()
  source=(host.SITE_ROOT/path).read_bytes();local=hashlib.sha256(source).hexdigest();live=hashlib.sha256(r.content).hexdigest()
  row={'path':path,'local_sha256':local,'live_sha256':live,'same':local==live,'raw_byte_identical':local==live}
  if not row['same'] and slug=='komplex-rita-funnel-2026-09-26-v1':
   # This task-owned preserved master receives the already observed CF email transform.
   # Accept it only when decoding restores exact source bytes AND pinned raw source matches.
   prior=json.loads(sorted((root/'evidence').glob('publication-20*.json'))[-1].read_text())
   pinned=requests.get(prior['deployment'].rstrip('/')+'/'+path,headers=headers,timeout=35);pinned.raise_for_status()
   decoded=[]
   def restore_anchor(match):
    encoded=re.search(rb'data-cfemail\s*=\s*[\"\x27]([0-9a-fA-F]+)',match.group(0)).group(1).decode('ascii')
    key=int(encoded[:2],16);address=bytes(int(encoded[i:i+2],16)^key for i in range(2,len(encoded),2));decoded.append(address.decode('utf-8'));return address
   restored=re.sub(rb'<a\b(?=[^>]*\b__cf_email__\b)(?=[^>]*\bdata-cfemail\s*=)[^>]*>[\s\S]*?</a>',restore_anchor,r.content,flags=re.I)
   restored=re.sub(rb'<script\b[^>]*\bsrc\s*=\s*[\"\x27]/cdn-cgi/scripts/5c5dd728/cloudflare-static/email-decode\.min\.js[\"\x27][^>]*>\s*</script>',b'',restored,flags=re.I)
   exact=len(decoded)==3 and set(decoded)=={'pelda@example.com'} and restored==source and pinned.content==source
   row.update(same=exact,known_owned_cf_email_transform_only=exact,decoded_addresses=decoded,pinned_raw_sha256=hashlib.sha256(pinned.content).hexdigest(),normalized_delivery_sha256=hashlib.sha256(restored).hexdigest())
  return row
 with concurrent.futures.ThreadPoolExecutor(max_workers=8) as pool:rows=list(pool.map(compare,[slug for slug in pages if slug not in changed]))
 differences=[row for row in rows if not row['same']]
 proof={'at':datetime.datetime.now(datetime.timezone.utc).isoformat(),'live_routes':len(pages),'unrelated_roots_replayed':len(rows),'differences':differences,'all_unrelated_roots_byte_identical':all(row['raw_byte_identical'] for row in rows),'all_unrelated_roots_source_preserved':not differences,'known_owned_cf_response_transforms':sum(bool(row.get('known_owned_cf_email_transform_only')) for row in rows),'rows':rows}
 (root/'evidence'/f'preservation-preflight-{timestamp}.json').write_text(json.dumps(proof,indent=2))
 assert not differences,'Unrelated live root changed since snapshot. Recover its complete asset dependencies before publishing.'
 return {'live_routes':len(pages),'unrelated_roots_replayed':len(rows),'all_unrelated_roots_byte_identical':all(row['raw_byte_identical'] for row in rows),'all_unrelated_roots_source_preserved':True,'known_owned_cf_response_transforms':sum(bool(row.get('known_owned_cf_email_transform_only')) for row in rows)}
with open('/tmp/clientsflow-review-host.lock','a') as lock:
 fcntl.flock(lock,fcntl.LOCK_EX)
 fresh_preservation=fresh_live_preservation()
 existing_worker=(host.SITE_ROOT/'_worker.js').read_bytes()
 canonical_worker=(Path('/Users/agency/.agents/skills/comment-html/assets/pages-worker.mjs')).read_bytes()
 assert existing_worker==canonical_worker,'Shared Worker differs from the audited existing proxy source. Preserve and re-audit its routes before staging.'
 missing=host.missing_from_mirror(host.SITE_ROOT)
 assert not missing,f'Live routes absent from mirror: {missing}'
 before=digests();backup=root/'private/host-backups'/timestamp;backup.mkdir(parents=True)
 for name in changed|{'index.html','review-manifest.json','_worker.js','_routes.json'}:
  src=host.SITE_ROOT/name
  if src.is_dir():shutil.copytree(src,backup/name)
  elif src.is_file():shutil.copy2(src,backup/name)
 absent=[name for name in changed|{'_routes.json'} if not(host.SITE_ROOT/name).exists()]
 receipt={'timestamp':timestamp,'target_project':'cf-review','branch':'main','domain':'review.clientsflow.hu','staging_mirror':str(host.SITE_ROOT),'isolated_snapshot':bool(args.mirror),'changed_routes':sorted(changed),'backups':str(backup),'previously_absent_task_routes':absent,'unrelated_files':len(before),'preflight_live_manifest_preserved':True,'fresh_live_preservation':fresh_preservation,'deployment_timeout_seconds':args.deploy_timeout}
 (backup/'restore.json').write_text(json.dumps(receipt,indent=2))
 for source,doc,slug,assets in jobs:host.stage(str(source),doc,slug,str(assets) if assets else None,core)
 routing={'version':1,'include':['/','/__review-comments','/__review-comments/*','/__review-image','/__review-image/*'],'exclude':[]}
 (host.SITE_ROOT/'_routes.json').write_text(json.dumps(routing,indent=2))
 total=host.write_index(host.SITE_ROOT)
 all_files=[p for p in host.SITE_ROOT.rglob('*') if p.is_file()]
 assert len(all_files)<=20000,f'Staged snapshot exceeds existing Free file allowance: {len(all_files)}'
 assert all(p.stat().st_size<=25*1024**2 for p in all_files),'An asset exceeds the existing static allowance'
 after=digests();assert before==after,'An unrelated file changed while staging under lock'
 assert not host.missing_from_mirror(host.SITE_ROOT),'Live route disappeared from staged manifest'
 print(f'Staged {len(jobs)} assembled batches. {len(before)} unrelated files preserved. Deploying verified cf-review project.',flush=True)
 try:pinned=core.deploy(host.SITE_ROOT,'cf-review','main')
 except Exception:
  for name in changed|{'index.html','review-manifest.json','_worker.js','_routes.json'}:
   saved=backup/name;current=host.SITE_ROOT/name
   if saved.is_dir():
    if current.exists():shutil.rmtree(current)
    shutil.copytree(saved,current)
   elif saved.is_file():shutil.copy2(saved,current)
   elif name in absent and current.is_dir():shutil.rmtree(current)
   elif name in absent and current.is_file():current.unlink()
  raise
 receipt.update(deployment=pinned,stable_urls=[f'https://review.clientsflow.hu/{s}/' for s in sorted(changed)],host_page_count=total,unrelated_hashes_preserved=True,static_routes_skip_function=True,comment_identity={slug:doc for _,doc,slug,_ in jobs})
 (root/'evidence'/f'publication-{timestamp}.json').write_text(json.dumps(receipt,indent=2))
 print(json.dumps(receipt,indent=2),flush=True)
