"""Schedule A packet writer; no changes to other forms or signed template bytes."""
import hashlib
import json
import os
from pathlib import Path
import re

from lxml import etree as E

DATA_NS = 'http://www.xfa.org/schema/xfa-data/1.0/'
PART = re.compile(r'([^\[\].]+)(?:\[(\d+)\])?')


def dataset_node(root, path, *, create=False, kinds=None, namespace=None):
    node = root
    if not kinds and E.QName(node).localname == 'datasets':
        node = next(c for c in node if E.QName(c).localname == 'data')
    for offset, segment in enumerate(path.split('.')):
        match = PART.fullmatch(segment)
        if not match:
            raise ValueError('Invalid Schedule A path')
        name, index = match[1], int(match[2] or 0)
        children = [c for c in node if isinstance(c.tag, str) and
                    (c.get('name') if kinds else E.QName(c).localname) == name]
        if index >= len(children) and not create:
            raise ValueError('Unresolved Schedule A path: ' + path)
        while index >= len(children):
            child = (E.SubElement(node, '{' + namespace + '}' + kinds[offset], name=name)
                     if kinds else E.SubElement(node, name))
            children.append(child)
        node = children[index]
    return node


def fill_schedule_a(payload, output_path, blank_path):
    if payload.get('form') != 'IMM5669':
        raise ValueError('Schedule A writer rejects other forms')
    language = payload.get('language', 'en')
    if language not in ('en', 'fr'):
        raise ValueError('Unsupported Schedule A language')
    contract = json.loads(Path(__file__).with_name(f'imm5669-contract.{language}.json').read_text(encoding='utf8'))
    blank, output = Path(blank_path), Path(output_path)
    if blank.resolve() == output.resolve():
        raise ValueError('Cannot overwrite official blank')
    if hashlib.sha256(blank.read_bytes()).hexdigest() != contract['sha256']:
        raise ValueError('Schedule A template requires review')
    fields = payload.get('fields')
    if fields == []:
        fields = {}
    if not isinstance(fields, dict):
        raise ValueError('Schedule A fields must be an object')
    data = E.Element('{' + DATA_NS + '}datasets', nsmap={'xfa': DATA_NS})
    data_root = E.SubElement(data, '{' + DATA_NS + '}data')
    form = E.Element('{' + contract['form_namespace'] + '}form', nsmap={None: contract['form_namespace']})
    for path, value in fields.items():
        meta = contract['fields'].get(path)
        if meta is None:
            raise ValueError('Unallowlisted Schedule A field: ' + str(path))
        if not isinstance(value, str) or len(value) > meta['max_chars']:
            raise ValueError('Invalid Schedule A value: ' + path)
        if meta.get('allowed_values') and value not in meta['allowed_values']:
            raise ValueError('Invalid Schedule A export: ' + path)
        dataset_node(data_root, meta['data_path'], create=True).text = value
        widget = dataset_node(form, meta['template_path'], create=True,
                              kinds=meta['kinds'], namespace=contract['form_namespace'])
        # Choice groups bind their selected export from datasets. Unlike fields,
        # XFA exclGroup nodes cannot contain a saved-form value element.
        if meta['kinds'][-1] == 'exclGroup':
            continue
        val = E.SubElement(widget, '{' + contract['form_namespace'] + '}value')
        E.SubElement(val, '{' + contract['form_namespace'] + '}' + meta['value_type']).text = value
    # Laravel's process environment can omit SystemRoot; cryptography needs it.
    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
    output.parent.mkdir(parents=True, exist_ok=True)
    with blank.open('rb') as source:
        writer = IncrementalPdfFileWriter(source)
        if writer.prev.encrypted:
            writer.encrypt('')
        acro = writer.root['/AcroForm']
        xfa = acro['/XFA']
        template = next(xfa[i + 1].data for i in range(0, len(xfa), 2) if str(xfa[i]) == 'template')
        datasets = E.tostring(data)
        form.set('checksum', form_checksum(template, datasets))
        updates = {'datasets': datasets, 'form': E.tostring(form)}
        entries = list(xfa)
        insert = next((i for i in range(0, len(entries), 2) if str(entries[i]) == 'postamble'), len(entries))
        for name, raw in updates.items():
            stream = generic.StreamObject(stream_data=raw)
            stream.compress()
            entries[insert:insert] = [generic.TextStringObject(name), writer.add_object(stream)]
            insert += 2
        acro['/XFA'] = generic.ArrayObject(entries)
        writer.update_container(acro)
        with output.open('wb') as target:
            writer.write(target)
    return output
