"""IMM1295 contact state; preserve official scripts and processing instructions."""
import re
from xml.parsers import expat

ADDRESS_SELECTION = 'ContactInformation.contact.SameAsMailingIndicator'
RESIDENTIAL_FIELDS = [
    'ResidentialAddressRow1.AptUnit.AptUnit', 'ResidentialAddressRow1.StreetNum.StreetNum',
    'ResidentialAddressRow1.StreetName.Streetname', 'ResidentialAddressRow1.CityTown.CityTown',
    'ResidentialAddressRow2.Country.Country', 'ResidentialAddressRow2.ProvinceState.ProvinceState',
    'ResidentialAddressRow2.PostalCode.PostalCode', 'ResidentialAddressRow2.District',
]


def address_values(fields, language='en'):
    result = dict(fields)
    if fields.get(ADDRESS_SELECTION) == 'Y':
        for name in RESIDENTIAL_FIELDS:
            if language == 'fr':
                name = name.replace('Country.Country', 'Pays.Pays')
            result[name] = ''
    return result


def contact_address_state(xml_bytes, fields):
    from imm5708_pdf_state import contact_address_state as patch_address
    names = [name.replace('Country.Country', 'Pays.Pays')
             if 'ResidentialAddressRow2.Pays.Pays' in fields else name
             for name in RESIDENTIAL_FIELDS]
    # No retains the official country-dependent rules for a separate address.
    return patch_address(xml_bytes, fields, selection=ADDRESS_SELECTION,
                         residential_base='ContactInformation.contact.',
                         names=names if fields.get(ADDRESS_SELECTION) == 'Y' else [])


def contact_phone_state(xml_bytes: bytes, fields: dict[str, str]) -> bytes:
    states = {}
    for group in ('Phone', 'AltPhone'):
        prefix = f'PhoneNumbers.{group}.'
        canada = fields.get(prefix + 'CanadaUS')
        other = fields.get(prefix + 'Other')
        # Mapper omits the inactive checkbox instead of writing its zero export.
        # A selected checkbox is sufficient; contradictory selections stay untouched.
        if (canada == '1' and other in (None, '0')) or (other == '1' and canada in (None, '0')):
            # EN and FR blanks share this exact phone container.
            for root in ('form1.Page2.ContactInformation.contact.',):
                states[root + prefix + 'NANumber'] = 'visible' if canada == '1' else 'invisible'
                states[root + 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
