"""Preserving native pass observers, including actual B-release/facing metadata.

Explicit reset-PC contracts. The final native BDE0 continuation is RTS here;
actual pass physics and all input games remain separate mandatory checks.
"""
from pathlib import Path
from concurrent.futures import ThreadPoolExecutor
import hashlib,json,struct
from native_probe import NativeProbe

def verify(rom,code,here,out,profile="directional"):
 h=Path(here);o=Path(out);o.mkdir(parents=True,exist_ok=True);image=Path(rom).read_bytes();probe=NativeProbe(rom,h)
 RAM_START,RAM_SIZE,PACKET=0xB000,0x4900,76
 seed=bytes((i*37+i//251+23)&255 for i in range(RAM_SIZE));sram=bytes((i*43+17)&255 for i in range(65536))
 contexts=[dict(kind=kind,irq=irq,ccr=ccr)for kind in('entry','final')for irq in range(8)for ccr in range(32)]
 def batch(index,mutant=None):
  ss=contexts[8*index:8*(index+1)];memory=bytearray(seed)
  body=[' lea 0x290000,%a0',' lea 0xffffb000,%a1',f' move.w #{RAM_SIZE//4-1},%d0','init_copy: move.l (%a0)+,(%a1)+',' dbra %d0,init_copy'];expected=[]
  for j,z in enumerate(ss):
   actor=6*(j%2);obj=0xB04A+128*actor;facing=(j*3+z['irq'])%8
   vals=[(obj+0x34,2,4),(obj+0x52,2,actor),(obj+0x54,2,facing),(obj+0x62,1,0x88|(64 if actor else 0)),
         (0xB772,2,(-16384+j)&65535),(0xB774,2,(-128,0,128)[j%3]&65535),(0xF0D4,2,j%3),(0xEF80,2,0),
         (0xBEDC,2,(facing+1)%8),(0xF0FC,2,1),(0xF0FE,2,3)]
   ticks=(0,1,9,15,29,30,7,22)[j%8]
   port=2*(j%2);bank_negative=bool(j%3);charge_at=0xF110+4*(port+(0 if bank_negative else 4))
   if profile=='charge':vals += [(0xF008,2,1),(0xC318,2,0x8000 if bank_negative else 0),(charge_at,4,0xFFFF0000|obj),(charge_at+4,2,ticks),(charge_at+6,2,2)]
   body+=[' move.w #0x2700,%sr']
   for at,size,value in vals:
    body.append(f' move.{"b"if size==1 else"l"if size==4 else"w"} #0x{value:x},0xffff{at:04x}');memory[at-RAM_START:at-RAM_START+size]=value.to_bytes(size,'big')
   regs=[0x11223300+i for i in range(8)]+[0xFFFF1000+256*i for i in range(7)]
   regs[2]=0x11220000|(16 if j%2 else 0)
   if profile=='charge':regs[4]=0x11220000|port
   regs[11]=0xFFFF0000|obj
   body += [f' move.l #0x{v:x},%d{i}'for i,v in enumerate(regs[:8])]+[f' movea.l #0x{v:x},%a{i}'for i,v in enumerate(regs[8:])]
   sr=0x2000|(z['irq']<<8)|z['ccr'];at=0xFFFF4000+PACKET*j
   body += [f' move.w #0x{sr:x},%sr',' jsr 0x2d9000']
   if z['kind']=='final':body+=[' jsr 0x2d9400']
   body += [f' move.w %sr,0x{at+60:x}',f' movem.l %d0-%d7/%a0-%a6,0x{at:x}',f' move.l %sp,0x{at+62:x}',
            f' move.w 0xffffef78,0x{at+66:x}',f' move.w 0xffffef7a,0x{at+68:x}',f' move.w 0xffffef80,0x{at+70:x}',
            f' move.w 0xffffea9c,0x{at+72:x}',f' move.w 0xffffea9e,0x{at+74:x}']
   expected.append((regs,sr,facing,ticks))
  tail=PACKET*len(ss)
  body += [' move.w #0x2700,%sr',' lea 0xffffb000,%a0',f' lea 0x{0xFFFF4000+tail:x},%a1',f' move.w #{RAM_SIZE//4-1},%d0','ram_copy: move.l (%a0)+,(%a1)+',' dbra %d0,ram_copy']
  payload=bytearray(code)
  if mutant=='D7':
   pos=payload.index(bytes.fromhex('4cdf7fff46df4e75'))+6
   assert payload[pos:pos+4]==bytes.fromhex('4e750000');payload[pos:pos+4]=bytes.fromhex('42874e75')
  if mutant=='release-source':
   if profile=='charge':
    pos=payload.index(bytes.fromhex('31e80004ef7a'));payload[pos+2:pos+4]=bytes.fromhex('0006')
   else:
    pos=payload.index(bytes.fromhex('31c2ef78'));payload[pos:pos+4]=bytes.fromhex('31c3ef78')
  p=o/(f'{mutant}-{index}'if mutant else f'case-{index}')
  raw=probe.run(p,'\n'.join(body),tail+RAM_SIZE,[(0x2D9000,b'\xff'*len(payload),bytes(payload)),(0xBDE0,image[0xBDE0:0xBDE2],bytes.fromhex('4e75'))],data=seed,data_address=0x290000,sram_data=sram,frames=100)
  for j,(z,(regs,sr,facing,ticks))in enumerate(zip(ss,expected)):
   at=PACKET*j;wanted=regs[:]
   if z['kind']=='final':
    vx=(-16384+j)&65535;vy=(-128,0,128)[j%3]&65535;wanted[0]=(wanted[0]&0xFFFF0000)|vx;wanted[1]=(wanted[1]&0xFFFF0000)|vy
    sr=(sr&~15)|(4 if vy==0 else 8 if vy&32768 else 0)
   got=(*struct.unpack_from('>15I',raw,at),*struct.unpack_from('>HI',raw,at+60))
   assert got==(*wanted,sr,0xFFFF9800),('pass observer native GPR/SR/IRQ/SP',z,got,wanted,sr)
   metadata=struct.unpack_from('>5H',raw,at+66)
   assert metadata[:3]==((1,ticks,int(z['kind']=='final'))if profile=='charge'else(regs[2]&65535,facing,int(z['kind']=='final'))),('pass observer actual release/facing metadata',z,metadata)
   if z['kind']=='final':assert metadata[3:]==((ticks,1)if profile=='charge'else(regs[2]&65535,facing)),('pass observer ring metadata',z,metadata)
  old,new=bytearray(memory),bytearray(raw[tail:])
  # Existing declared test-only ring/scratch. Everything outside it, including
  # actor/puck/controller/RNG/cache/settings fields, must be byte-exact.
  for lo,hi in((0xEA00,0xEF00),(0xEF00,0xEF82)):old[lo-RAM_START:hi-RAM_START]=new[lo-RAM_START:hi-RAM_START]
  assert old==new,'pass observer changed game fields outside its declared private packets'
  assert(p/'probe.srm').read_bytes()==sram,'pass observer changed64KB SRAM'
  return ss
 with ThreadPoolExecutor(max_workers=3)as pool:rows=sum(pool.map(batch,range(64)),[])
 negatives=[]
 for mutant,needle in(('D7','GPR/SR/IRQ/SP'),('release-source','actual release/facing metadata')):
  try:batch(0,mutant)
  except AssertionError as e:
   assert needle in str(e),str(e);negatives.append(dict(mutant=mutant,rejected=True,reason=str(e)[:180]))
  else:raise AssertionError(('pass observer mutant not rejected',mutant))
 report=dict(ROM_sha256=hashlib.sha256(image).hexdigest(),observer_sha256=hashlib.sha256(code).hexdigest(),profile=profile,contexts=len(rows),live_Momentum_E900_buffer_untouched=True,all_IRQ_CCR_GPR_SR_SP=True,
             native_displaced_D0_D1_CCR=True,all_other_game_RAM_exact=True,whole64KB_SRAM_exact=True,active_negatives=negatives)
 (o/'results.json').write_text(json.dumps(report,indent=2)+'\n');print('PASS512 native pass observer full context/RAM/SRAM plus2 active mutants',flush=True);return report
