"""IMM5562-only incremental writer. Signed template and official events unchanged."""
from copy import deepcopy
import hashlib
import json
import os
from pathlib import Path
import re
from lxml import etree as E

PART=re.compile(r'([^\[\].]+)(?:\[(\d+)\])?')

def resolve(root,path,*,saved=False,create=False,namespace=None):
    node=root
    if not saved and E.QName(node).localname=='datasets':
        node=next(c for c in node if isinstance(c.tag,str) and E.QName(c).localname=='data')
    pieces=path.split('.')
    for i,piece in enumerate(pieces):
        match=PART.fullmatch(piece)
        if not match:raise ValueError('Invalid IMM5562 path')
        name,index=match[1],int(match[2] or 0)
        same=[c for c in node if isinstance(c.tag,str) and (c.get('name') if saved else E.QName(c).localname)==name]
        if match[2] is None and len(same)>1:raise ValueError('Ambiguous IMM5562 path: '+path)
        if index>=len(same) and not create:raise ValueError('Unresolved IMM5562 path: '+path)
        while index>=len(same):
            kind='field' if i==len(pieces)-1 else 'subform'
            same.append(E.SubElement(node,'{'+namespace+'}'+kind,name=name) if saved else E.SubElement(node,name))
        node=same[index]
    return node

def fill_travels(payload,output_path,blank_path):
    if payload.get('form')!='IMM5562':raise ValueError('IMM5562 writer rejects other forms')
    lang=payload.get('language','en')
    if lang not in ('en','fr'):raise ValueError('Unsupported IMM5562 language')
    c=json.loads(Path(__file__).with_name(f'imm5562-contract.{lang}.json').read_text(encoding='utf8'))
    blank,out=Path(blank_path),Path(output_path)
    if blank.resolve()==out.resolve():raise ValueError('Cannot overwrite original blank')
    if hashlib.sha256(blank.read_bytes()).hexdigest()!=c['sha256']:raise ValueError('IMM5562 template requires review')
    fields=payload.get('fields');counts=payload.get('imm5562_rows',{});state=payload.get('imm5562_state',{})
    if fields==[]:fields={}
    if not isinstance(fields,dict) or not isinstance(counts,dict) or not isinstance(state,dict):raise ValueError('Invalid IMM5562 payload')
    if set(counts)-{'applicant','spouse','dependents'} or set(state)-{'partner','dependents'}:raise ValueError('Unallowlisted IMM5562 state')
    dep=counts.get('dependents',[])
    if not isinstance(dep,list) or any(type(v) is not int or v<0 for v in [counts.get('applicant',0),counts.get('spouse',0),*dep]):raise ValueError('Invalid IMM5562 row count')
    if any(v is not None and type(v) is not bool for v in state.values()):raise ValueError('Invalid IMM5562 visibility')
    allowed={path:{'max_chars':200,'value_type':'text'} for path in c['identity'].values()}
    def path(value,index=0,row=0):return value.replace('{dependent}',str(index)).replace('{row}',str(row))
    for group,definition in c['groups'].items():
        for index,count in enumerate(dep if group=='dependents' else [counts.get(group,0)]):
            allowed[path(definition['selector'],index)]={'max_chars':1,'value_type':'integer','allowed_values':['0','1']}
            if definition['name']:allowed[path(definition['name'],index)]={'max_chars':400,'value_type':'text'}
            for row in range(count):
                for key,meta in definition['fields'].items():
                    allowed[path(meta['path'],index,row)]={**meta,**({'allowed_values':list(c['purposes'].values())} if key=='purpose' else {})}
    for key,value in fields.items():
        if key not in allowed:raise ValueError('Unallowlisted IMM5562 field: '+str(key))
        meta=allowed[key]
        if not isinstance(value,str) or len(value)>meta['max_chars']:raise ValueError('Invalid IMM5562 field value: '+key)
        if meta.get('allowed_values') and value not in meta['allowed_values']:raise ValueError('Invalid IMM5562 export: '+key)
    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 system 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)
        if writer.prev.encrypted:writer.encrypt('')
        x=writer.root['/AcroForm']['/XFA'];packets={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,form=E.fromstring(packets['datasets'],parser),E.fromstring(packets['form'],parser)
        ns=c['form_namespace']
        def saved_container(value):
            # Resolver's final leaf defaults to field; containers are subforms.
            node=resolve(form,value,saved=True,create=True,namespace=ns)
            node.tag='{'+ns+'}subform'
            return node
        def expand(value,wanted):
            first=resolve(data,value);parent=first.getparent();name=E.QName(first).localname
            rows=[n for n in parent if isinstance(n.tag,str) and E.QName(n).localname==name]
            while len(rows)<wanted:
                new=deepcopy(first);parent.append(new);rows.append(new)
            first_saved=saved_container(value);parent=first_saved.getparent()
            rows=[n for n in parent if n.get('name')==name]
            while len(rows)<wanted:
                new=deepcopy(first_saved);parent.append(new);rows.append(new)
        expand(path(c['groups']['dependents']['container']),max(1,len(dep)))
        for group,definition in c['groups'].items():
            for index,count in enumerate(dep if group=='dependents' else [counts.get(group,0)]):
                saved_container(path(definition['container'],index))
                wanted=max(definition['initial_rows'],count)
                expand(path(definition['row'],index),wanted)
                # The blank saves its third-row controls as visible. Move that
                # state to the final instance instead of leaving two button sets.
                for row in range(wanted):
                    saved_container(path(definition['row'],index,row)+'.buttons').set('presence','visible' if row==wanted-1 else 'hidden')
        for key,value in fields.items():
            resolve(data,key).text=value
            widget=resolve(form,key,saved=True,create=True,namespace=ns)
            for old in widget.xpath('./*[local-name()="value"]'):widget.remove(old)
            v=E.SubElement(widget,'{'+ns+'}value');E.SubElement(v,'{'+ns+'}'+allowed[key]['value_type']).text=value
        for key,value in [('partner',c['groups']['spouse']['container']),('dependents','IMM_5562.Page1.Item2C-Z')]:
            if state.get(key) is not None:saved_container(value).set('presence','visible' if state[key] else 'hidden')
        for node in list(form.xpath('//*[local-name()="subform"]')):
            parent=node.getparent();name=node.get('name')
            if not name or E.QName(parent).localname=='form':continue
            if not any(isinstance(child.tag,str) and E.QName(child).localname=='instanceManager' and child.get('name')=='_'+name for child in parent):parent.insert(parent.index(node),E.Element('{'+ns+'}instanceManager',name='_'+name))
        datasets=E.tostring(data);form.set('checksum',form_checksum(packets['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
