"""Schedule-1-only allowlisted dataset creation and dynamic saved-form instances.

Template packet stays byte-identical (scripts, bindings, namespaces, PIs intact).
No other IMM form uses this module. This does not execute Adobe validation.
"""
from copy import deepcopy
import hashlib
import json
from pathlib import Path
import re

from lxml import etree as E
import pikepdf

GROUPS = ('military', 'ill_treatment', 'organizations', 'government', 'travel')
XFA_DATA = 'http://www.xfa.org/schema/xfa-data/1.0/'
FORM_NS = 'http://www.xfa.org/schema/xfa-form/2.8/'


def parse(data):
    return E.fromstring(data, E.XMLParser(remove_blank_text=False, remove_comments=False, remove_pis=False, resolve_entities=False))


def child(parent, name):
    matches = [c for c in parent if isinstance(c.tag, str) and E.QName(c).localname == name]
    if len(matches) > 1:
        raise ValueError('Ambiguous Schedule-1 dataset node: '+name)
    return matches[0] if matches else E.SubElement(parent, name)


def named(root, path, create=False, final_kind='field'):
    parts = path.split('.')
    nodes = [root]
    for index, part in enumerate(parts):
        candidates = [c for n in nodes for c in n if c.get('name') == part]
        if not candidates and create:
            if len(nodes) != 1:
                raise ValueError('Ambiguous form state path: '+path)
            kind = final_kind if index == len(parts)-1 else 'subform'
            candidates = [E.SubElement(nodes[0], '{'+FORM_NS+'}'+kind, name=part)]
        nodes = candidates
    return nodes


def set_form_value(node, value):
    values = [c for c in node if E.QName(c).localname == 'value']
    for c in values:
        node.remove(c)
    val = E.SubElement(node, '{'+FORM_NS+'}value')
    E.SubElement(val, '{'+FORM_NS+'}text').text = value


def fill_schedule(payload, output_path, blank_path):
    from prefill_imm_xfa_pdf import release_edit_lock

    if payload.get('form') != 'IMM5257SCH1':
        raise ValueError('Schedule-1 writer rejects other forms')
    language = payload.get('language', 'en')
    if language not in ('en', 'fr'):
        raise ValueError('Unsupported Schedule-1 language')
    contract = json.loads((Path(__file__).parent/f'imm5257sch1-contract.{language}.json').read_text(encoding='utf-8'))
    blank = Path(blank_path)
    if hashlib.sha256(blank.read_bytes()).hexdigest() != contract['sha256']:
        raise ValueError('Schedule-1 template requires review')
    fields = payload.get('fields', {})
    if fields == []:
        fields = {}
    counts = payload.get('schedule_rows', {})
    if not isinstance(fields, dict) or not isinstance(counts, dict):
        raise ValueError('Invalid Schedule-1 payload')
    for key, value in fields.items():
        pattern = re.sub(r'\[\d+\]', '[{row}]', key)
        meta = contract['fields'].get(pattern)
        if meta is None or not isinstance(value, str) or not value:
            raise ValueError('Unallowlisted or invalid Schedule-1 field: '+key)
        if len(value) > meta.get('max_chars', 2000):
            raise ValueError('Schedule-1 field too long: '+key)
        if 'allowed_values' in meta and value not in meta['allowed_values']:
            raise ValueError('Invalid Schedule-1 export: '+key)
        if 'choices' in meta and value not in meta['choices']:
            raise ValueError('Invalid Schedule-1 country: '+key)
    with pikepdf.open(blank) as pdf:
        xfa = pdf.Root.AcroForm.XFA
        streams = {str(xfa[i]): xfa[i+1] for i in range(0,len(xfa),2)}
        data = parse(streams['datasets'].read_bytes())
        data_node = data.find('{'+XFA_DATA+'}data')
        schedule = data_node.find('Schedule1') if data_node is not None else None
        if schedule is None:
            raise ValueError('Missing Schedule1 dataset')
        form = parse(streams['form'].read_bytes()) if 'form' in streams else None
        # Remove only allowlisted business nodes. LOVs and unrelated dataset content stay.
        top = {p.split('.')[1] for p in contract['fields']}
        top.update(('ApplicationValidationDate', 'FormValidated'))
        for node in list(schedule):
            if E.QName(node).localname in top:
                schedule.remove(node)
        for group, (table_path, table) in zip(GROUPS, contract['tables'].items()):
            count = counts.get(group, 0)
            if type(count) is not int or count < 0:
                raise ValueError('Invalid Schedule-1 row count')
            # No arbitrary business cap: all supplied rows preserved in input order.
            prefix, leaf = table_path.rsplit('.',1)
            parent = schedule
            for part in prefix.split('.')[1:]:
                parent = child(parent, part)
            leaf = leaf.replace('[{row}]','')
            for _ in range(count):
                E.SubElement(parent, leaf)
            if form is not None:
                row_path = table['template_path']
                form_rows = named(form,row_path)
                if not form_rows:
                    raise ValueError('Missing saved Schedule-1 row prototype')
                prototype = deepcopy(form_rows[0])
                form_parent = form_rows[0].getparent()
                position = form_parent.index(form_rows[0])
                for row in form_rows:
                    form_parent.remove(row)
                for index in range(count):
                    row = deepcopy(prototype)
                    row.set('presence','visible')
                    # Blank saved state may contain placeholders. Rebind only actual values.
                    for n in list(row.iter()):
                        if E.QName(n).localname=='value':
                            n.getparent().remove(n)
                    form_parent.insert(position+index,row)
        for key, value in fields.items():
            parent = schedule
            for part in key.split('.')[1:]:
                match = re.fullmatch(r'([A-Za-z][A-Za-z0-9]*)(?:\[(\d+)\])?',part)
                if not match:
                    raise ValueError('Invalid Schedule-1 node path')
                name, index = match.groups()
                if index is None:
                    parent = child(parent,name)
                else:
                    children=[c for c in parent if E.QName(c).localname==name]
                    if int(index)>=len(children):
                        raise ValueError('Schedule-1 row index exceeds declared rows')
                    parent=children[int(index)]
            parent.text=value
            parent.set('{'+XFA_DATA+'}dataNode','dataValue')
            if form is not None:
                pattern = re.sub(r'\[\d+\]', '[{row}]', key)
                meta=contract['fields'][pattern]
                path=meta['template_path']
                index_match=re.search(r'\[(\d+)\]',key)
                if index_match:
                    table=next(t for p,t in contract['tables'].items() if pattern.startswith(p+'.'))
                    rows=named(form,table['template_path'])
                    tail=path[len(table['template_path'])+1:]
                    nodes=named(rows[int(index_match[1])],tail,create=True)
                else:
                    nodes=named(form,path,create=True,final_kind='exclGroup' if 'allowed_values' in meta else 'field')
                for node in nodes:set_form_value(node,value)
        # Stale 2012 date in blank must never imply a validated draft.
        for part in ('Year','Month','Day'):
            child(child(schedule,'ApplicationValidationDate'),part).text=None
        child(schedule,'FormValidated').text=None
        if form is not None:
            for node in form.iter():
                if node.get('name') in ('ApplicationValidatedFlag',):set_form_value(node,'')
            # Clear validation-date fields by actual template binding paths.
            template=parse(streams['template'].read_bytes())
            def clear_validation(node, path=''):
                if not isinstance(node.tag,str):return
                path += ('.'+node.get('name') if node.get('name') else '')
                if any(isinstance(c.tag,str) and E.QName(c).localname=='bind' and c.get('ref','').startswith('$.ApplicationValidationDate.') for c in node):
                    for field in named(form,path.lstrip('.')):set_form_value(field,'')
                for c in node:clear_validation(c,path)
            clear_validation(template)
            streams['form'].write(E.tostring(form))
        streams['datasets'].write(E.tostring(data))
        release_edit_lock(pdf)
        out=Path(output_path)
        out.parent.mkdir(parents=True,exist_ok=True)
        pdf.save(out)
    return out
