"""IMM1344-only incremental XFA writer. Original signed template stays unchanged."""
from pathlib import Path
import hashlib,json,os
from lxml import etree as E

def replace_answer_data(raw, datasets):
 """Change only xfa:data, preserving official lookup bytes and XML framing."""
 from xml.parsers import expat
 namespace='http://www.xfa.org/schema/xfa-data/1.0/'
 parser=expat.ParserCreate(namespace_separator='}')
 span=[]
 depth=0
 active=None
 def start(tag,attrs):
  nonlocal depth,active
  depth+=1
  if tag==namespace+'}data':
   if span:raise ValueError('Ambiguous IMM1344 data packet')
   span.append(parser.CurrentByteIndex);active=depth
 def end(tag):
  nonlocal depth
  if active==depth and tag==namespace+'}data':
   span.append(raw.index(b'>',parser.CurrentByteIndex)+1)
  depth-=1
 parser.StartElementHandler=start;parser.EndElementHandler=end
 parser.Parse(raw,True)
 nodes=datasets.xpath('./*[local-name()="data"]')
 if len(span)!=2 or len(nodes)!=1:raise ValueError('Missing IMM1344 data packet')
 return raw[:span[0]]+E.tostring(nodes[0],encoding='utf-8',with_tail=False)+raw[span[1]:]

def resolve(root,path,kinds=None,ns=None):
 node=root
 if kinds is None:
  matches=root.xpath('./*[local-name()="data"]')
  if len(matches)!=1:raise ValueError('Missing IMM1344 data packet')
  node=matches[0]
 for i,name in enumerate(path.split('.')):
  found=[n for n in node if isinstance(n.tag,str) and (n.get('name') if kinds else E.QName(n).localname)==name]
  if len(found)>1:raise ValueError('Ambiguous IMM1344 path: '+path)
  if not found:
   if not kinds:raise ValueError('Unresolved IMM1344 dataset: '+path)
   found=[E.SubElement(node,'{'+ns+'}'+kinds[i],name=name)]
  node=found[0]
 return node

def restore_on_open(form,template,contract,fields,ns):
 """Keep saved UI state and restore postal answers cleared by initial country merge.

 Always run the official saved-delta restore before optional hydration. Adobe
 defers saved access/presence changes until this restore; skipping it leaves
 North American country code editable and incorrectly validates code 1.
 Template and all validation scripts remain untouched.
 """
 if not fields:return
 root=resolve(form,'form1',['subform'],ns)
 original=E.fromstring(template).xpath('//*[local-name()="subform" and @name="form1"]')[0]
 postals=[(path,value) for path,value in fields.items() if path.endswith('.PostalCode')]
 if postals:
  initial=original.xpath('./*[local-name()="event" and @activity="initialize"]')[0]
  event=E.SubElement(root,'{'+ns+'}event',**dict(initial.attrib))
  capture=[''.join(initial.xpath('./*[local-name()="script"]/text()'))]
  for i,(path,value) in enumerate(postals):
   country=path.rsplit('.',1)[0]+'.Country'
   capture.append('var state=xfa.datasets.data.form1.imm1344PrefillState; var d=xfa.datasets.data.resolveNode('+json.dumps(path)+'); var c=xfa.datasets.data.resolveNode('+json.dumps(country)+'); var postal=d && d.value ? String(d.value) : ""; var country=c && c.value ? String(c.value) : ""; state.resolveNode("postal'+str(i)+'").value=postal; state.resolveNode("country'+str(i)+'").value=country; state.resolveNode("armed'+str(i)+'").value=postal ? "1" : "0"; var countries=xfa.resolveNodes("xfa.datasets.LOVFile.LOV.CountryOfBirthList.CountryOfBirth[*]"); var label=""; for(var j=0;j<countries.length;j++) if(String(countries.item(j).lic.value)===country) label=String(countries.item(j).value); state.resolveNode("label'+str(i)+'").value=label;')
  E.SubElement(event,'{'+ns+'}script',contentType='application/x-javascript',runAt='client').text='\n'.join(capture)
 original_root=original
 original=original.xpath('./*[local-name()="event" and @activity="docReady"]')[0]
 event=E.SubElement(root,'{'+ns+'}event',**dict(original.attrib))
 commands=[''.join(original.xpath('./*[local-name()="script"]/text()')),'var imm1344Restore = xfa.datasets.data.form1.resolveNode("imm1344PrefillState");','if (imm1344Restore && String(imm1344Restore.resolveNode("active").value) === "1") {']
 co='form1.Part1.SponsorDetails.genDetails.q3.CoSignerInd'
 if co in fields:
  commands.append('var co=xfa.form.resolveNode('+json.dumps(co)+'); if(co && (co.rawValue==="Y" || co.rawValue==="N")) xfa.form.resolveNode("form1.Part1.CoSigner").presence=co.rawValue==="Y" ? "visible" : "hidden";')
 phone_start=len(commands)
 for meta in contract['answers'].values():
  if meta['type']!='phone':continue
  p=meta['phone'];f=p['fields']
  if f['CanadaUS'] not in fields:continue
  # Set access before assigning code 1: rawValue changes can enqueue validation.
  commands.append('var cd=xfa.datasets.data.resolveNode('+json.dumps(f['CanadaUS'])+'); var od=xfa.datasets.data.resolveNode('+json.dumps(f['Other'])+'); var north=cd && String(cd.value)==="1"; var other=od && String(od.value)==="1"; var cc=xfa.form.resolveNode('+json.dumps(f['NumberCountry'])+'); if(cc && (north||other)) cc.access=north?"readOnly":"open";')
  # Reader can restore a checkbox to 0 while its dataset says 1. Assigning that
  # checkbox runs the template click, which hides the other number layout and
  # clears the extension. Show the layout from the saved dataset, then write
  # the digits again so that click cannot leave them blank.
  repair=[]
  for name,path in f.items():
   if name=='ActualNumber':continue
   line='var n=xfa.form.resolveNode('+json.dumps(path)+'); var d=xfa.datasets.data.resolveNode('+json.dumps(path)+'); if(n && d && d.value!=null) { var v=d.value; if(String(n.rawValue)!==String(v)) n.rawValue=v; }'
   commands.append(line)
   if name not in ('CanadaUS','Other'):repair.append(line)
  commands.append('var cd=xfa.datasets.data.resolveNode('+json.dumps(f['CanadaUS'])+'); var od=xfa.datasets.data.resolveNode('+json.dumps(f['Other'])+'); var north=cd && String(cd.value)==="1"; var other=od && String(od.value)==="1"; var c=xfa.form.resolveNode('+json.dumps(f['CanadaUS'])+'); var o=xfa.form.resolveNode('+json.dumps(f['Other'])+'); if(north||other){ if(north){ if(o && String(o.rawValue)!=="0") o.rawValue="0"; if(c && String(c.rawValue)!=="1") c.rawValue="1"; } else { if(c && String(c.rawValue)!=="0") c.rawValue="0"; if(o && String(o.rawValue)!=="1") o.rawValue="1"; } }')
  commands.extend(repair)
  commands.append('var na=xfa.form.resolveNode('+json.dumps(p['root']+'.NANumber')+'); var intl=xfa.form.resolveNode('+json.dumps(p['root']+'.IntlNumber')+'); if(north||other){ if(na) na.presence=north?"visible":"invisible"; if(intl) intl.presence=north?"invisible":"visible"; } var cc=xfa.form.resolveNode('+json.dumps(f['NumberCountry'])+'); if(cc && (north||other)) cc.access=north?"readOnly":"open";')
 # docReady is too late for Reader's initial validation queue. Establish the
 # phone branch before LOV initialization as well, using current datasets.
 # Keep docReady repair for values affected by subsequent initialization.
 phone_commands=commands[phone_start:]
 if phone_commands:
  events=root.xpath('./*[local-name()="event" and @activity="initialize"]')
  if events:
   initial_script=events[0].xpath('./*[local-name()="script"]')[0]
  else:
   initial=original_root.xpath('./*[local-name()="event" and @activity="initialize"]')[0]
   initial_event=E.SubElement(root,'{'+ns+'}event',**dict(initial.attrib))
   initial_script=E.SubElement(initial_event,'{'+ns+'}script',contentType='application/x-javascript',runAt='client')
   initial_script.text=''.join(initial.xpath('./*[local-name()="script"]/text()'))
  initial_script.text='(function(){\n'+'\n'.join(phone_commands)+'\n})();\n'+(initial_script.text or '')
 commands.append('}')
 E.SubElement(event,'{'+ns+'}script',contentType='application/x-javascript',runAt='client').text='\n'.join(commands)
 for i,(path,value) in enumerate(postals):
  country=path.rsplit('.',1)[0]+'.Country'
  widget=resolve(form,country,contract['fields'][country]['kinds'],ns)
  tree=E.fromstring(template)
  original_country=resolve(tree,country,contract['fields'][country]['kinds'],E.QName(tree).namespace)
  original_change=original_country.xpath('./*[local-name()="event" and @activity="change"]')[0]
  change=E.SubElement(widget,'{'+ns+'}event',**dict(original_change.attrib))
  flag='xfa.datasets.data.form1.imm1344PrefillState.resolveNode("armed'+str(i)+'")'
  state='xfa.datasets.data.form1.imm1344PrefillState'
  guard='var state='+state+'; var imm1344KeepPostal = String('+flag+'.value)==="1" && String(this.rawValue)===String(state.resolveNode("country'+str(i)+'").value) && String(PostalCode.rawValue)===String(state.resolveNode("postal'+str(i)+'").value) && (!xfa.event.newText || String(xfa.event.newText)===String(state.resolveNode("country'+str(i)+'").value) || String(xfa.event.newText)===String(state.resolveNode("label'+str(i)+'").value));\n'
  code=''.join(original_change.xpath('./*[local-name()="script"]/text()'))
  if 'PostalCode.rawValue = null;' not in code:raise ValueError('IMM1344 country-change script requires review')
  code=code.replace('PostalCode.rawValue = null;','if (!imm1344KeepPostal) PostalCode.rawValue = null;')
  E.SubElement(change,'{'+ns+'}script',contentType='application/x-javascript',runAt='client').text=guard+code
  # A real user entering Country ends initial-merge protection permanently for
  # this open session; original country-change behavior then remains intact.
  enter=E.SubElement(widget,'{'+ns+'}event',name='imm1344PostalUserEdit',activity='enter')
  E.SubElement(enter,'{'+ns+'}script',contentType='application/x-javascript',runAt='client').text=flag+'.value="0";'


def fill_sponsorship(payload,output_path,blank_path):
 if payload.get('form')!='IMM1344':raise ValueError('Wrong form for IMM1344 writer')
 lang=payload.get('language','en')
 if lang not in ['en','fr']:raise ValueError('Unsupported IMM1344 language')
 c=json.loads(Path(__file__).with_name(f'imm1344-contract.{lang}.json').read_text(encoding='utf8'))
 blank,out=Path(blank_path),Path(output_path)
 if blank.resolve()==out.resolve():raise ValueError('Cannot overwrite official template')
 if hashlib.sha256(blank.read_bytes()).hexdigest()!=c['sha256']:raise ValueError('IMM1344 template requires review')
 fields=payload.get('fields')
 if fields==[]:fields={}
 if not isinstance(fields,dict):raise ValueError('Invalid IMM1344 fields')
 for path,value in fields.items():
  m=c['fields'].get(path)
  if not m:raise ValueError('Unallowlisted IMM1344 field: '+str(path))
  if not isinstance(value,str) or len(value)>m['max_chars']:raise ValueError('Invalid IMM1344 field value: '+path)
  if m['choices'] and value not in m['choices']:raise ValueError('Invalid IMM1344 export: '+path)
 if os.name=='nt' and not os.environ.get('SystemRoot'):
  import ctypes
  buffer=ctypes.create_unicode_buffer(32768)
  if not ctypes.windll.kernel32.GetWindowsDirectoryW(buffer,len(buffer)):raise OSError('Cannot resolve Windows directory')
  os.environ['SystemRoot']=buffer.value
 from pyhanko.pdf_utils.incremental_writer import IncrementalPdfFileWriter
 from pyhanko.pdf_utils import generic
 from imm0008_xfa_checksum import form_checksum
 out.parent.mkdir(parents=True,exist_ok=True)
 with blank.open('rb') as source:
  writer=IncrementalPdfFileWriter(source,strict=False)
  if writer.prev.encrypted:writer.encrypt('')
  x=writer.root['/AcroForm']['/XFA'];pk={str(x[i]):x[i+1].data for i in range(0,len(x),2)}
  parser=E.XMLParser(remove_comments=False,remove_pis=False,resolve_entities=False)
  data=E.fromstring(pk['datasets'],parser);form=E.fromstring(pk['form'],parser);ns=c['form_namespace']
  for path,value in fields.items():
   m=c['fields'][path];resolve(data,path).text=value
   widget=resolve(form,path,m['kinds'],ns)
   # Ordinary answers bind from datasets; phone UI values are saved below
   # together with access/presence for the template's manual delta restore.
   for old in widget.xpath('./*[local-name()="value"]'):widget.remove(old)
   if not path.endswith('.ActualNumber'):widget.set('access','open')
  if fields:
   state=E.SubElement(resolve(data,'form1'),'imm1344PrefillState');E.SubElement(state,'active').text='1'
   for i,path in enumerate(path for path in fields if path.endswith('.PostalCode')):
    for name in ['postal','country','label','armed']:E.SubElement(state,name+str(i)).text='0'
  def sub(path):return resolve(form,path,['subform']*len(path.split('.')),ns)
  co=fields.get('form1.Part1.SponsorDetails.genDetails.q3.CoSignerInd')
  if co is not None:sub('form1.Part1.CoSigner').set('presence','visible' if co=='Y' else 'hidden')
  for key,m in c['answers'].items():
   if m['type']!='phone':continue
   p=m['phone'];f=p['fields'];north=fields.get(f['CanadaUS']);other=fields.get(f['Other'])
   if north is None and other is None:continue
   for name,path in f.items():
    if path not in fields or name=='ActualNumber':continue
    meta=c['fields'][path];widget=resolve(form,path,meta['kinds'],ns)
    val=E.SubElement(widget,'{'+ns+'}value')
    E.SubElement(val,'{'+ns+'}'+meta['value_type']).text=fields[path]
   resolve(form,f['NumberCountry'],c['fields'][f['NumberCountry']]['kinds'],ns).set('access','readOnly' if north=='1' else 'open')
   sub(p['root']+'.NANumber').set('presence','visible' if north=='1' else 'invisible')
   sub(p['root']+'.IntlNumber').set('presence','invisible' if north=='1' else 'visible')
   # Saved UI state is explicit; never invoke click events which erase digits.
   for name in ['NANumber.AreaCode','NANumber.FirstThree','NANumber.LastFive','IntlNumber.IntlNumber']:
    resolve(form,f[name],c['fields'][f[name]]['kinds'],ns).set('access','open')
  for address in ['sponsor_mailing','sponsor_residential']:
   country=c['answers'][address+'.country']['paths'][0];code=fields.get(country)
   if code is None:continue
   province=c['answers'][address+'.province']['paths'][0];widget=resolve(form,province,c['fields'][province]['kinds'],ns)
   widget.set('access','open' if code in ['511','461'] else 'readOnly')
   lookup='StateAbbrevList.StateAbbrev[*]' if code=='461' else 'ProvinceAbbrevList.ProvinceAbbrev[*]'
   for old in widget.xpath('./*[local-name()="bindItems"]'):widget.remove(old)
   E.SubElement(widget,'{'+ns+'}bindItems',ref='xfa.datasets.LOVFile.LOV.'+lookup,labelRef='$',valueRef='lic')
  for n in list(form.xpath('//*[local-name()="subform"]')):
   parent=n.getparent();name=n.get('name')
   if E.QName(parent).localname=='form' or not name:continue
   if not any(E.QName(ch).localname=='instanceManager' and ch.get('name')=='_'+name for ch in parent if isinstance(ch.tag,str)):
    parent.insert(parent.index(n),E.Element('{'+ns+'}instanceManager',name='_'+name))
  # Verify every submitted target before appending the incremental revision.
  if any(resolve(data,path).text!=value for path,value in fields.items()):raise ValueError('IMM1344 payload/XML mismatch')
  restore_on_open(form,pk['template'],c,fields,ns)
  datasets=replace_answer_data(pk['datasets'],data);form.set('checksum',form_checksum(pk['template'],datasets))
  updates={'datasets':datasets,'form':E.tostring(form)}
  for i in range(0,len(x),2):
   name=str(x[i])
   if name in updates:
    ref=x.raw_get(i+1);stream=generic.StreamObject(stream_data=updates[name]);stream.compress();writer.objects[(ref.generation,ref.idnum)]=stream
  with out.open('wb') as target:writer.write(target)
 return out
