"""Strict address-space observations; no guessed ranges or host mutations (Python 3.8)."""
import ipaddress
import json
import re

FAILURES = frozenset({
    'inventory_command_failed', 'inventory_timeout', 'inventory_output_limit', 'inventory_invalid',
    'network_inventory_changed', 'host_inventory_changed', 'network_ipam_unsupported',
    'host_namespace_unverified', 'docker_endpoint_unsupported', 'docker_engine_unsupported',
    'pool_provenance_unknown', 'pool_inventory_invalid', 'host_routes_unsupported',
    'host_rules_unsupported', 'host_addresses_invalid', 'resolver_inventory_invalid',
    'resolver_upstream_unknown',
}) | frozenset('host_rules_unsupported_' + part for part in
              ('count', 'fields', 'priority', 'source', 'table', 'structure', 'fwmark', 'fwmask', 'action', 'duplicate'))
PRIVATE = tuple(ipaddress.ip_network(value) for value in ('10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16'))
DEVICE = re.compile(r'[A-Za-z0-9][A-Za-z0-9_.:-]{0,14}')

# Exact tags were read and compared; source hashes are in network_pool_facts.json.
BUILTIN_VERSIONS = frozenset({
    '20.10.0', '20.10.1', '20.10.2', '20.10.3', '20.10.4', '20.10.5', '20.10.6', '20.10.7',
    '20.10.8', '20.10.9', '20.10.10', '20.10.11', '20.10.12', '20.10.13', '20.10.14', '20.10.15',
    '20.10.16', '20.10.17', '20.10.18', '20.10.19', '20.10.20', '20.10.21', '20.10.22', '20.10.23',
    '20.10.24', '20.10.25', '20.10.26', '20.10.27', '23.0.0', '23.0.1', '23.0.2', '23.0.3',
    '23.0.4', '23.0.5', '23.0.6', '23.0.7', '23.0.8', '23.0.9', '23.0.10', '23.0.11',
    '23.0.12', '23.0.13', '23.0.14', '23.0.15', '23.0.16', '23.0.17', '23.0.18', '24.0.0',
    '24.0.1', '24.0.2', '24.0.3', '24.0.4', '24.0.5', '24.0.6', '24.0.7', '24.0.8',
    '24.0.9', '25.0.0', '25.0.1', '25.0.2', '25.0.3', '25.0.4', '25.0.5', '25.0.7',
    '25.0.8', '25.0.9', '25.0.10', '25.0.11', '25.0.12', '25.0.14', '25.0.15', '25.0.16',
    '26.0.0', '26.0.1', '26.0.2', '26.1.0', '26.1.1', '26.1.2', '26.1.3', '26.1.4',
    '27.0.1', '27.0.2', '27.0.3', '27.1.0', '27.1.1', '27.1.2', '27.2.0', '27.2.1',
    '27.3.0', '27.3.1', '27.4.0', '27.4.1', '27.5.0', '27.5.1', '28.0.0', '28.0.1',
    '28.0.2', '28.0.3', '28.0.4', '28.1.0', '28.1.1', '28.2.0', '28.2.1', '28.2.2',
    '28.3.0', '28.3.1', '28.3.2', '28.3.3', '28.4.0', '28.5.0', '28.5.1', '28.5.2',
})
BUILTIN_POOLS = (
    {'Base': '172.17.0.0/16', 'Size': 16},
    {'Base': '172.18.0.0/16', 'Size': 16},
    {'Base': '172.19.0.0/16', 'Size': 16},
    {'Base': '172.20.0.0/14', 'Size': 16},
    {'Base': '172.24.0.0/14', 'Size': 16},
    {'Base': '172.28.0.0/14', 'Size': 16},
    {'Base': '192.168.0.0/16', 'Size': 20},
)


class InventoryFailure(Exception):
    def __init__(self, code):
        self.code = code if code in FAILURES else 'inventory_invalid'
        self.details = {}
        super().__init__(self.code)


def unique_object(pairs):
    result = {}
    for key, value in pairs:
        if key in result:
            raise InventoryFailure('inventory_invalid')
        result[key] = value
    return result


def decode(raw):
    try:
        return json.loads(raw, object_pairs_hook=unique_object, parse_constant=reject_constant)
    except (ValueError, TypeError, UnicodeError, RecursionError):
        raise InventoryFailure('inventory_invalid') from None


def reject_constant(value):
    raise InventoryFailure('inventory_invalid')


def prefix(value, *, strict=False):
    if not isinstance(value, str) or len(value) > 128 or '%' in value:
        raise InventoryFailure('inventory_invalid')
    try:
        return ipaddress.ip_network(value, strict=strict)
    except ValueError:
        raise InventoryFailure('inventory_invalid') from None


def effective_pools(raw, version):
    """Only exact reviewed release versions permit the empty-info builtin fallback."""
    source = 'engine_info'
    if raw is None or raw == []:
        if version not in BUILTIN_VERSIONS:
            raise InventoryFailure('pool_provenance_unknown')
        raw, source = list(BUILTIN_POOLS), 'versioned_moby_builtin'
    if not isinstance(raw, list) or not raw or len(raw) > 64:
        raise InventoryFailure('pool_inventory_invalid')
    result = []
    for row in raw:
        if not isinstance(row, dict) or set(row) != {'Base', 'Size'}:
            raise InventoryFailure('pool_inventory_invalid')
        base, size = prefix(row['Base'], strict=True), row['Size']
        if type(size) is not int or not base.prefixlen <= size <= base.max_prefixlen:
            raise InventoryFailure('pool_inventory_invalid')
        if base.version == 6:
            continue
        if not any(base.subnet_of(space) for space in PRIVATE):
            raise InventoryFailure('pool_inventory_invalid')
        if any(base.overlaps(prefix(other['base'])) for other in result):
            raise InventoryFailure('pool_inventory_invalid')
        result.append({'base': str(base), 'size': size})
    if not result:
        raise InventoryFailure('pool_inventory_invalid')
    return result, source


def route_exclusions(rows, records=None):
    """All IPv4 tables are observed; unsupported forwarding mechanisms refuse."""
    allowed = {'dst', 'type', 'gateway', 'prefsrc', 'src', 'dev', 'protocol', 'scope', 'table',
               'metric', 'flags', 'tos', 'mtu', 'advmss', 'initcwnd', 'initrwnd', 'rto_min',
               'quickack', 'congctl', 'features', 'expires', 'pref', 'nexthops', 'multipath'}
    result, records = set(), records if records is not None else []
    if not isinstance(rows, list) or not rows or len(rows) > 8192:
        raise InventoryFailure('host_routes_unsupported')
    for row in rows:
        if (not isinstance(row, dict) or set(row) - allowed or 'dst' not in row
                or row.get('type', 'unicast') not in
                {'unicast', 'local', 'broadcast', 'multicast', 'blackhole', 'unreachable', 'prohibit', 'throw'}):
            raise InventoryFailure('host_routes_unsupported')
        device, kind = row.get('dev', ''), 'route_other'
        if (not isinstance(device, str) or (device and not DEVICE.fullmatch(device))
                or any(type(row.get(key, '')) not in (str, int) for key in ('scope', 'table', 'protocol'))):
            raise InventoryFailure('host_routes_unsupported')
        table = {254: 'main', 255: 'local'}.get(row.get('table'), row.get('table', 'main'))
        signature = (row.get('type', 'unicast'), row.get('scope'), table)
        if row.get('protocol') == 'kernel' and not set(row) - {
                'dst', 'type', 'dev', 'protocol', 'scope', 'table', 'prefsrc', 'flags'}:
            kind = {('unicast', 'link', 'main'): 'route_connected', ('local', 'host', 'local'): 'route_local',
                    ('broadcast', 'link', 'local'): 'route_broadcast'}.get(signature, 'route_other')
        if row['dst'] != 'default':
            destination = prefix(row['dst'])
            if destination.version != 4 or destination.prefixlen == 0:
                raise InventoryFailure('host_routes_unsupported')
            result.add(str(destination))
            records.append({'subnet': str(destination), 'kind': kind, 'device': device})
        elif row.get('table') in (52, '52'):
            # A Tailscale exit route precedes main and can divert a new bridge's own traffic.
            raise InventoryFailure('host_routes_unsupported')
        paths = row.get('nexthops', row.get('multipath', []))
        if not isinstance(paths, list) or len(paths) > 256:
            raise InventoryFailure('host_routes_unsupported')
        for path in [row] + paths:
            if not isinstance(path, dict) or (path is not row and set(path) - {'gateway', 'dev', 'weight', 'flags'}):
                raise InventoryFailure('host_routes_unsupported')
            device = path.get('dev', '')
            if not isinstance(device, str) or (device and not DEVICE.fullmatch(device)):
                raise InventoryFailure('host_routes_unsupported')
            for key in ('gateway', 'prefsrc', 'src'):
                if key in path:
                    address = prefix(path[key])
                    if address.version != 4:
                        raise InventoryFailure('host_routes_unsupported')
                    result.add(str(address))
                    observed = kind + '_source' if key == 'prefsrc' else 'route_' + key
                    records.append({'subnet': str(address), 'kind': observed, 'device': device})
    return result


def address_exclusions(rows, records=None):
    result, records = set(), records if records is not None else []
    if not isinstance(rows, list) or not rows or len(rows) > 4096:
        raise InventoryFailure('host_addresses_invalid')
    for row in rows:
        if not isinstance(row, dict) or not isinstance(row.get('addr_info'), list):
            raise InventoryFailure('host_addresses_invalid')
        device = row.get('ifname')
        if not isinstance(device, str) or not DEVICE.fullmatch(device):
            raise InventoryFailure('host_addresses_invalid')
        for address in row['addr_info']:
            if (not isinstance(address, dict) or address.get('family') != 'inet'
                    or type(address.get('prefixlen')) is not int or not 0 <= address['prefixlen'] <= 32):
                raise InventoryFailure('host_addresses_invalid')
            local = prefix(address.get('local'))
            if local.version != 4 or local.prefixlen != 32:
                raise InventoryFailure('host_addresses_invalid')
            subnet = str(prefix(str(local.network_address) + '/' + str(address['prefixlen'])))
            result.add(subnet)
            records.append({'subnet': subnet, 'kind': 'address_network', 'device': device})
            records.append({'subnet': str(local), 'kind': 'address_local', 'device': device})
            for key in ('peer', 'broadcast'):
                if key in address:
                    peer = prefix(address[key])
                    if peer.version != 4:
                        raise InventoryFailure('host_addresses_invalid')
                    result.add(str(peer))
                    records.append({'subnet': str(peer), 'kind': 'address_' + key, 'device': device})
    if not result:
        raise InventoryFailure('host_addresses_invalid')
    return result


def validate_rules(rows):
    """Admit ordinary Linux rules or the exact Tailscale v1.88.2 baseIPRules chain."""
    # https://github.com/tailscale/tailscale/blob/v1.88.2/wgengine/router/router_linux.go
    if not isinstance(rows, list) or len(rows) not in (3, 7):
        raise InventoryFailure('host_rules_unsupported_count')
    expected = [{'priority': p, 'src': 'all', 'table': t}
                for p, t in ((0, 'local'), (32766, 'main'), (32767, 'default'))]
    if len(rows) == 7:
        marked = {'src': 'all', 'fwmark': '0x80000', 'fwmask': '0xff0000'}
        expected += [dict(marked, priority=5210, table='main'), dict(marked, priority=5230, table='default'),
                     dict(marked, priority=5250, action='unreachable'), {'priority': 5270, 'src': 'all', 'table': 52}]
    actual = []
    # iproute2 iprule.c prints table names/numbers as JSON strings, including Tailscale's "52".
    tables = {255: 'local', 254: 'main', 253: 'default',
              '255': 'local', '254': 'main', '253': 'default', '52': 52}
    for row in rows:
        if not isinstance(row, dict) or set(row) - {'priority', 'src', 'table', 'protocol',
                                                   'fwmark', 'fwmask', 'action'}:
            raise InventoryFailure('host_rules_unsupported_fields')
        if type(row.get('priority')) is not int:
            raise InventoryFailure('host_rules_unsupported_priority')
        if row.get('src') != 'all':
            raise InventoryFailure('host_rules_unsupported_source')
        normalized = {key: value for key, value in row.items() if key != 'protocol'}
        table = normalized.get('table')
        if table is not None:
            if type(table) not in (str, int):
                raise InventoryFailure('host_rules_unsupported_table')
            normalized['table'] = tables.get(table, table)
        wanted = next((item for item in expected if item['priority'] == normalized['priority']), None)
        if wanted is None:
            raise InventoryFailure('host_rules_unsupported_priority')
        if set(normalized) != set(wanted):
            raise InventoryFailure('host_rules_unsupported_structure')
        for field in ('table', 'fwmark', 'fwmask', 'action'):
            if normalized.get(field) != wanted.get(field):
                raise InventoryFailure('host_rules_unsupported_' + field)
        if normalized in actual:
            raise InventoryFailure('host_rules_unsupported_duplicate')
        actual.append(normalized)
    if len(actual) != len(expected):
        raise InventoryFailure('host_rules_unsupported_count')


def resolver_exclusions(raw, *, resolved=False, records=None):
    """Parse resolver addresses only; local stubs require authoritative upstream data."""
    try:
        lines = raw.decode('utf-8').splitlines()
    except (AttributeError, UnicodeError):
        raise InventoryFailure('resolver_inventory_invalid') from None
    result, stub, observed = set(), False, False
    records = records if records is not None else []
    for line in lines:
        if resolved:
            match = re.fullmatch(r'(?:Global|Link [0-9]+ \([^\r\n:]+\)):\s*(.*)', line.strip())
            if not match:
                raise InventoryFailure('resolver_inventory_invalid')
            words = match[1].split()
        else:
            words = line.split('#', 1)[0].split(';', 1)[0].split()
            if not words or words[0] != 'nameserver':
                continue
            if len(words) != 2:
                raise InventoryFailure('resolver_inventory_invalid')
            words = words[1:]
        for word in words:
            address = prefix(word)
            observed = True
            if address.prefixlen != address.max_prefixlen:
                raise InventoryFailure('resolver_inventory_invalid')
            if address.version != 4:
                continue
            if address.network_address.is_loopback:
                if resolved or str(address.network_address) not in {'127.0.0.53', '127.0.0.54'}:
                    raise InventoryFailure('resolver_upstream_unknown')
                stub = True
            result.add(str(address))
            records.append({'subnet': str(address), 'kind': 'resolver', 'device': ''})
    if not observed:
        raise InventoryFailure('resolver_inventory_invalid')
    return result, stub
