"""Keep IMM5708 contact widgets consistent with confirmed dataset selections."""
import re
from xml.parsers import expat

ADDRESS_SELECTION = 'ContactInformation.Resi.SameAsAddr.SameAsMailingInd'
RESIDENTIAL_FIELDS = [
    'AddrLine1.AptUnit', 'AddrLine1.StreetNum', 'AddrLine1.Streetname',
    'AddrLine2.City', 'AddrLine2.Country', 'AddrLine2.Prov', 'AddrLine2.PostalCode', 'AddrLine2.District',
]


def address_values(fields):
    """Clear inactive residential values without changing caller answers."""
    result = dict(fields)
    if fields.get(ADDRESS_SELECTION) == 'Y':
        for name in RESIDENTIAL_FIELDS:
            result['ContactInformation.Resi.' + name] = ''
    return result


def contact_address_state(xml_bytes, fields, *, selection=ADDRESS_SELECTION,
                          residential_base='ContactInformation.Resi.', names=None):
    """Patch saved XFA state, preserving scripts, namespaces and PIs byte for byte."""
    names = RESIDENTIAL_FIELDS if names is None else names
    choice = fields.get(selection)
    if choice not in ('Y', 'N'):
        return xml_bytes
    parser = expat.ParserCreate(namespace_separator='}')
    stack, nodes = [], []

    def start(tag, attrs):
        path = stack[-1]['path'] if stack else ''
        if attrs.get('name'):
            path = (path + '.' + attrs['name']).lstrip('.')
        offset = parser.CurrentByteIndex
        match = re.match(br'<(?:[^>"\']|"[^"]*"|\'[^\']*\')*>', xml_bytes[offset:])
        node = dict(tag=tag.rsplit('}', 1)[-1], path=path, start=offset,
                    opening_end=offset + match.end(), children=[])
        if stack:
            stack[-1]['children'].append(node)
        stack.append(node)
        nodes.append(node)

    def end(tag):
        node = stack.pop()
        node['end'] = (node['opening_end'] if xml_bytes[node['opening_end']-2:node['opening_end']] == b'/>'
                       else xml_bytes.index(b'>', parser.CurrentByteIndex) + 1)
    parser.StartElementHandler, parser.EndElementHandler = start, end
    parser.Parse(xml_bytes, True)
    edits = []
    group = 'form1.Page2.' + selection
    residential = 'form1.Page2.' + residential_base
    # Saved form packets omit unchanged widgets. Materialize only address state.
    if nodes[0]['tag'] == 'form':
        by_path = {n['path']: n for n in nodes if n['tag'] in ('subform', 'field', 'exclGroup')}
        additions = {}
        if group not in by_path:
            parts = selection.removeprefix(residential_base).split('.')
            content = ('<exclGroup name="' + parts[-1] + '">'
                '<field name="No"><value><text/></value></field>'
                '<field name="Yes"><value><text/></value></field>'
                '</exclGroup>')
            for part in reversed(parts[:-1]):
                content = '<subform name="' + part + '">' + content + '</subform>'
            additions[residential.rstrip('.')] = content
        for name in names:
            if residential + name not in by_path:
                parent, field = name.rsplit('.', 1)
                key = residential + parent
                additions[key] = additions.get(key, '') + '<field name="' + field + '"><value><text/></value><validate nullTest="disabled"/></field>'
        if additions:
            for path, content in additions.items():
                # Saved packets can omit an entire unchanged address container.
                while path not in by_path:
                    path, name = path.rsplit('.', 1)
                    content = '<subform name="' + name + '">' + content + '</subform>'
                parent = by_path[path]
                offset = parent['opening_end']
                edits.append((offset, offset, content.encode()))
            for begin, finish, replacement in sorted(edits, reverse=True):
                xml_bytes = xml_bytes[:begin] + replacement + xml_bytes[finish:]
            return contact_address_state(xml_bytes, fields, selection=selection,
                                         residential_base=residential_base, names=names)

    def attribute(node, name, value):
        opening = xml_bytes[node['start']:node['opening_end']]
        pattern = rb'\b' + name.encode() + rb'\s*=\s*["\'][^"\']*["\']'
        replacement = name.encode() + b'="' + value.encode() + b'"'
        if re.search(pattern, opening):
            opening = re.sub(pattern, replacement, opening, count=1)
        else:
            pos = len(opening) - (2 if opening.endswith(b'/>') else 1)
            opening = opening[:pos] + b' ' + replacement + opening[pos:]
        edits.append((node['start'], node['opening_end'], opening))

    def prefix_for(node):
        opening = xml_bytes[node['start']:node['opening_end']]
        return re.match(br'<((?:[^: >]+:)?)[^: >]+', opening)[1]

    def value(node, text):
        opening = xml_bytes[node['start']:node['opening_end']]
        prefix = prefix_for(node)
        content = b'<' + prefix + b'value><' + prefix + b'text>' + text.encode() + b'</' + prefix + b'text></' + prefix + b'value>'
        existing = next((c for c in node['children'] if c['tag'] == 'value'), None)
        if existing:
            edits.append((existing['start'], existing['end'], content))
        elif not opening.endswith(b'/>'):
            edits.append((node['opening_end'], node['opening_end'], content))

    for node in nodes:
        if node['path'] == group and node['tag'] == 'exclGroup':
            value(node, choice)
        elif node['tag'] == 'field' and node['path'] in (group + '.Yes', group + '.No'):
            selected = 'Y' if node['path'].endswith('.Yes') else 'N'
            value(node, selected if choice == selected else '')
        elif node['tag'] == 'field' and node['path'] in [residential + n for n in names]:
            attribute(node, 'access', 'readOnly' if choice == 'Y' or node['path'].endswith('.Country') else 'open')
            if choice == 'Y':
                value(node, '')
            for child in node['children']:
                if child['tag'] == 'validate':
                    required = not node['path'].endswith(('.AptUnit', '.StreetNum', '.District'))
                    attribute(child, 'nullTest', 'error' if choice == 'N' and required else 'disabled')
            if not any(c['tag'] == 'validate' for c in node['children']):
                required = choice == 'N' and not node['path'].endswith(('.AptUnit', '.StreetNum', '.District'))
                content = b'<' + prefix_for(node) + b'validate nullTest="' + (b'error' if required else b'disabled') + b'"/>'
                edits.append((node['opening_end'], node['opening_end'], content))
    for begin, finish, replacement in sorted(edits, key=lambda e: (e[0], e[1]), reverse=True):
        xml_bytes = xml_bytes[:begin] + replacement + xml_bytes[finish:]
    return xml_bytes


def contact_phone_state(xml_bytes: bytes, fields: dict[str, str]) -> bytes:
    states = {}
    for group, section in [('Phone', 'q3-4'), ('AltPhone', 'q3-4'), ('Fax', 'q5-6')]:
        prefix = f'ContactInformation.{section}.{group}.'
        canada = fields.get(prefix + 'CanOtherInd.CanadaUS')
        other = fields.get(prefix + 'CanOtherInd.Other')
        if (canada, other) in [('1', '0'), ('0', '1')]:
            states['form1.Page2.' + prefix + 'NANumber'] = 'visible' if canada == '1' else 'invisible'
            states['form1.Page2.' + prefix + 'IntlNumber'] = 'invisible' if canada == '1' else 'visible'
    if not states:
        return xml_bytes

    # XFA packets contain processing instructions and namespace-sensitive content.
    # Locate attributes with a parser, but preserve every other original byte.
    parser = expat.ParserCreate(namespace_separator='}')
    paths = []
    edits = []

    def start(tag, attrs):
        path = paths[-1] if paths else ''
        if attrs.get('name'):
            path = (path + '.' + attrs['name']).lstrip('.')
        paths.append(path)
        if path not in states or tag.rsplit('}', 1)[-1] != 'subform':
            return
        offset = parser.CurrentByteIndex
        end = offset
        quote = None
        while end < len(xml_bytes):
            char = xml_bytes[end]
            if quote is not None:
                if char == quote:
                    quote = None
            elif char in (34, 39):
                quote = char
            elif char == 62:
                break
            end += 1
        opening = xml_bytes[offset:end + 1]
        presence = states[path].encode('ascii')
        pattern = br'(\bpresence\s*=\s*[\x22\x27])[^\x22\x27]*([\x22\x27])'
        if re.search(pattern, opening):
            opening = re.sub(pattern, lambda m: m[1] + presence + m[2], opening, count=1)
        else:
            insert = len(opening) - (2 if opening.endswith(b'/>') else 1)
            opening = opening[:insert] + b' presence="' + presence + b'"' + opening[insert:]
        edits.append((offset, end + 1, opening))

    parser.StartElementHandler = start
    parser.EndElementHandler = lambda tag: paths.pop()
    parser.Parse(xml_bytes, True)
    for start, end, replacement in reversed(edits):
        xml_bytes = xml_bytes[:start] + replacement + xml_bytes[end:]
    return xml_bytes
