"""IMM5009-only incremental datasets writer. No template/script/permission edits."""
import hashlib
import json
import os
import re
from datetime import date
from pathlib import Path
from lxml import etree as E

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


def resolve(root, path):
    # IMM5009 contains literal dotted names (P.O.box). Match names rather than
    # splitting on dots, and require canonical occurrence indexes.
    if E.QName(root).localname == 'datasets':
        root = next(c for c in root if isinstance(c.tag,str) and E.QName(c).localname == 'data')
    def descend(node, remaining):
        counts = {}
        for child in node:
            if isinstance(child.tag,str):
                name = E.QName(child).localname
                counts[name] = counts.get(name,0) + 1
        seen = {}
        matches = []
        for child in node:
            if not isinstance(child.tag,str): continue
            name = E.QName(child).localname
            index = seen.get(name,0); seen[name] = index+1
            part = name + (f'[{index}]' if counts[name]>1 else '')
            if remaining == part: matches.append(child)
            elif remaining.startswith(part+'.'):
                matches.extend(descend(child, remaining[len(part)+1:]))
        return matches
    matches = descend(root,path)
    if len(matches)!=1: raise ValueError('Unresolved or ambiguous IMM5009 path: '+path)
    return matches[0]


def fill_document_request(payload, output_path, blank_path):
    if payload.get('form') != 'IMM5009': raise ValueError('IMM5009 writer rejects other forms')
    lang = payload.get('language','en')
    if lang not in ['en','fr']: raise ValueError('Unsupported IMM5009 language')
    c = json.loads(Path(__file__).with_name(f'imm5009-contract.{lang}.json').read_text(encoding='utf-8'))
    blank, out = Path(blank_path), Path(output_path)
    if blank.resolve() == out.resolve(): raise ValueError('Cannot overwrite original template')
    if hashlib.sha256(blank.read_bytes()).hexdigest() != c['sha256']: raise ValueError('IMM5009 template requires review')
    fields = payload.get('fields')
    if fields == []: fields = {}
    if not isinstance(fields,dict): raise ValueError('Invalid IMM5009 fields')
    for key,value in fields.items():
        if key not in c['fields']: raise ValueError('Unallowlisted IMM5009 field: '+str(key))
        meta = c['fields'][key]
        if not isinstance(value,str) or len(value) > meta['max_chars']: raise ValueError('Invalid IMM5009 value: '+key)
        if meta['choices'] and value not in meta['choices'].values(): raise ValueError('Invalid IMM5009 export: '+key)
        if meta.get('numeric') and not re.fullmatch(r'\d+(?:\.\d{1,2})?',value): raise ValueError('Invalid IMM5009 numeric value: '+key)
        if re.search(r'[\x00-\x08\x0b\x0c\x0e-\x1f]',value): raise ValueError('Invalid XML characters')
        target = next((name for name,path in c['targets'].items() if path == key), '')
        if target.endswith('_country_code') and not re.fullmatch(r'[1-9]\d{0,2}',value):
            raise ValueError('Invalid IMM5009 calling code')
        if target in ['phone_number','alternate_phone_number'] and not re.fullmatch(r'\d{4,15}',value):
            raise ValueError('Invalid IMM5009 phone number')
        if target.endswith('_extension') and not value.isascii() or target.endswith('_extension') and not value.isdigit():
            raise ValueError('Invalid IMM5009 extension')
        if meta['value_type'] == 'date' or target.endswith(('_issue','_expiry','_date')) or target == 'dob' or any(row.get('dob') == key for rows in c['groups'].values() for row in rows):
            try:
                if not re.fullmatch(r'\d{4}-\d{2}-\d{2}',value): raise ValueError()
                date.fromisoformat(value)
            except ValueError:
                raise ValueError('Invalid IMM5009 date: '+key)
    # PHP/Symfony can launch Windows children without SystemRoot. asyncio's
    # Windows socket provider needs it before pyHanko is imported.
    if os.name == 'nt' and not os.environ.get('SystemRoot'):
        import ctypes
        buffer = ctypes.create_unicode_buffer(32768)
        size = ctypes.windll.kernel32.GetWindowsDirectoryW(buffer, len(buffer))
        if not size or size >= 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
    with blank.open('rb') as source:
        writer = IncrementalPdfFileWriter(source,strict=False)
        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(resolve_entities=False,remove_comments=False,remove_pis=False)
        data = E.fromstring(packets['datasets'],parser)
        for key,value in fields.items(): resolve(data,key).text=value
        updated = E.tostring(data)
        # Reject discrepancies before producing any downloadable artifact.
        reparsed = E.fromstring(updated,parser)
        if any((resolve(reparsed,key).text or '') != value for key,value in fields.items()):
            raise ValueError('IMM5009 payload/XML mismatch')
        form = E.fromstring(packets['form'],parser)
        if len(form): raise ValueError('IMM5009 saved form state requires review')
        form.set('checksum',form_checksum(packets['template'],updated))
        updates = {'datasets':updated,'form':E.tostring(form)}
        for i in range(0,len(x),2):
            name = str(x[i])
            if name in updates:
                reference=x.raw_get(i+1)
                stream=generic.StreamObject(stream_data=updates[name]);stream.compress()
                writer.objects[(reference.generation,reference.idnum)] = stream
        with out.open('wb') as target: writer.write(target)
    return out
