"""Optional fixed DigitalOcean metadata readback; identity evidence never delegates Docker address space."""
import ipaddress
import re
import socket
import time

ORIGIN = ('169.254.169.254', 80)
PATHS = {
    'droplet_id': '/metadata/v1/id',
    'region': '/metadata/v1/region',
    'public_ipv4': '/metadata/v1/interfaces/public/0/ipv4/address',
    'private_ipv4': '/metadata/v1/interfaces/private/0/ipv4/address',
    'netmask': '/metadata/v1/interfaces/private/0/ipv4/netmask',
    'private_gateway': '/metadata/v1/interfaces/private/0/ipv4/gateway',
}
IDENTITY = ('droplet_id', 'region', 'public_ipv4', 'private_ipv4', 'private_subnet', 'private_gateway')
PRIVATE = tuple(ipaddress.IPv4Network(value) for value in ('10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16'))
REASONS = {
    'verified': {''},
    'mismatch': {'public_address_mismatch', 'public_address_unobserved', 'private_address_mismatch'},
    'unavailable': {'metadata_missing', 'metadata_unavailable', 'metadata_timeout'},
    'unverified': {'expected_host_unverified', 'host_inventory_unverified',
                   'metadata_invalid', 'metadata_response_limit'},
}


class MetadataFailure(ValueError):
    pass


def _timeout(deadline):
    remaining = deadline - time.monotonic()
    if remaining <= 0:
        raise MetadataFailure('metadata_timeout')
    return remaining


def _headers(connection, deadline):
    raw = bytearray()
    while not raw.endswith(b'\r\n\r\n'):
        if len(raw) >= 8192:
            raise MetadataFailure('metadata_response_limit')
        connection.settimeout(_timeout(deadline))
        value = connection.recv(1)
        if not value:
            raise MetadataFailure('metadata_invalid')
        raw.extend(value)
    lines = bytes(raw[:-4]).split(b'\r\n')
    if len(lines) > 17:
        raise MetadataFailure('metadata_response_limit')
    match = re.fullmatch(rb'HTTP/1\.[01] ([1-5][0-9]{2})(?: [\x20-\x7e]*)?', lines[0])
    if not match:
        raise MetadataFailure('metadata_invalid')
    if match[1] != b'200':
        raise MetadataFailure('metadata_missing' if match[1] == b'404' else 'metadata_unavailable')
    headers = {}
    for line in lines[1:]:
        match = re.fullmatch(rb"([!#$%&'*+.^_`|~0-9A-Za-z-]+):[ \t]*([\x20-\x7e\t]*)", line)
        if not match or match[1].lower() in headers:
            raise MetadataFailure('metadata_invalid')
        headers[match[1].lower()] = match[2].strip()
    if b'transfer-encoding' in headers or headers.get(b'content-encoding', b'identity') != b'identity':
        raise MetadataFailure('metadata_invalid')
    length = headers.get(b'content-length')
    if length is not None and not re.fullmatch(rb'[0-9]{1,3}', length):
        raise MetadataFailure('metadata_invalid')
    if length is not None and int(length) > 128:
        raise MetadataFailure('metadata_response_limit')
    return None if length is None else int(length)


def _fetch(path, deadline):
    """One HTTP/1.0 request, no proxy, DNS, redirect, authentication, discovery or retry."""
    if path not in PATHS.values():
        raise MetadataFailure('metadata_invalid')
    deadline = min(deadline, time.monotonic() + 2)
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as connection:
        connection.settimeout(_timeout(deadline))
        connection.connect(ORIGIN)
        connection.settimeout(_timeout(deadline))
        request = 'GET ' + path + ' HTTP/1.0\r\nHost: 169.254.169.254\r\nConnection: close\r\n\r\n'
        connection.sendall(request.encode('ascii'))
        length = _headers(connection, deadline)
        body = bytearray()
        limit = 129 if length is None else length
        while len(body) < limit:
            connection.settimeout(_timeout(deadline))
            value = connection.recv(limit - len(body))
            if not value:
                break
            body.extend(value)
        _timeout(deadline)
        if len(body) > 128:
            raise MetadataFailure('metadata_response_limit')
        if length is not None and len(body) != length:
            raise MetadataFailure('metadata_invalid')
        return bytes(body)


def _address(value):
    if not isinstance(value, str):
        raise ValueError()
    address = ipaddress.IPv4Address(value)
    if str(address) != value:
        raise ValueError()
    return address


def _identity(values):
    if (not re.fullmatch(r'[1-9][0-9]{0,19}', values['droplet_id'])
            or not re.fullmatch(r'[a-z][a-z0-9-]{1,31}', values['region'])):
        raise ValueError()
    _address(values['public_ipv4'])
    private, gateway = _address(values['private_ipv4']), _address(values['private_gateway'])
    subnet = ipaddress.IPv4Network(values['private_subnet'], strict=True)
    if (str(subnet) != values['private_subnet'] or subnet.prefixlen > 30
            or not any(subnet.subnet_of(space) for space in PRIVATE)
            or any(address not in subnet or address in (subnet.network_address, subnet.broadcast_address)
                   for address in (private, gateway)) or private == gateway):
        raise ValueError()


def _read_values(fetch, deadline):
    values = {}
    for field, path in PATHS.items():
        leaf_deadline = min(deadline, time.monotonic() + 2)
        _timeout(leaf_deadline)
        raw = fetch(path, leaf_deadline)
        _timeout(leaf_deadline)
        if not isinstance(raw, bytes):
            raise MetadataFailure('metadata_invalid')
        if len(raw) > 128:
            raise MetadataFailure('metadata_response_limit')
        values[field] = raw.decode('ascii').strip(' \t\r\n')
    mask = values.pop('netmask')
    mask_network = ipaddress.IPv4Network('0.0.0.0/' + mask)
    if str(mask_network.netmask) != mask:
        raise ValueError()
    values['private_subnet'] = str(ipaddress.IPv4Network(values['private_ipv4'] + '/' + mask, strict=False))
    _identity(values)
    return values


def _binding(inventory, values, expected_host):
    records = inventory['host_records']
    bridges = {'docker0'}
    for network in inventory['networks']:
        if network['Driver'] == 'bridge':
            bridges.add('br-' + network['Id'][:12])
            bridges.add((network.get('Options') or {}).get('com.docker.network.bridge.name', ''))
    facts = {}
    for row in records:
        facts.setdefault((row['kind'], row['subnet']), set()).add(row['device'])
    if values['public_ipv4'] != expected_host:
        return 'public_address_mismatch', 'unverified'
    public = facts.get(('address_local', values['public_ipv4'] + '/32'), set())
    if len(public) != 1 or public & bridges or any(not device or device.startswith('br-') for device in public):
        return 'public_address_unobserved', 'unverified'
    local = facts.get(('address_local', values['private_ipv4'] + '/32'), set())
    network = facts.get(('address_network', values['private_subnet']), set())
    if (len(local) != 1 or local != network or local & bridges
            or any(not device or device.startswith('br-') for device in local)):
        return 'private_address_mismatch', 'unverified'
    gateways = facts.get(('route_gateway', values['private_gateway'] + '/32'), set())
    return '', 'observed' if local & gateways else 'not_observed'


def observe(inventory, expected_host, fetch=None):
    """Called only by explicit provider diagnostics; fetch(path, deadline) permits offline fixtures."""
    result = dict.fromkeys(IDENTITY, '')
    result.update(provider='digitalocean', status='unverified', reason='expected_host_unverified',
                  private_gateway_route='unverified')
    try:
        _address(expected_host)
    except (ValueError, TypeError):
        return result
    if (not isinstance(inventory, dict) or inventory.get('complete') is not True
            or not isinstance(inventory.get('host_records'), list) or not isinstance(inventory.get('networks'), list)):
        return dict(result, reason='host_inventory_unverified')
    try:
        values = _read_values(fetch or _fetch, time.monotonic() + 10)
    except Exception as error:
        reason = str(error) if isinstance(error, MetadataFailure) else 'metadata_invalid'
        if isinstance(error, (socket.timeout, TimeoutError)):
            reason = 'metadata_timeout'
        elif isinstance(error, OSError):
            reason = 'metadata_unavailable'
        if reason not in REASONS['unavailable'] | REASONS['unverified']:
            reason = 'metadata_invalid'
        return dict(result, reason=reason, status='unavailable' if reason in REASONS['unavailable'] else 'unverified')
    try:
        reason, route = _binding(inventory, values, expected_host)
    except Exception:
        return dict(result, reason='host_inventory_unverified')
    result.update(values, status='mismatch' if reason else 'verified', reason=reason, private_gateway_route=route)
    return result


def validate_provider(receipt):
    fields = set(IDENTITY) | {'provider', 'status', 'reason', 'private_gateway_route'}
    if (not isinstance(receipt, dict) or set(receipt) != fields
            or any(not isinstance(value, str) for value in receipt.values())
            or receipt['provider'] != 'digitalocean' or receipt['status'] not in REASONS
            or receipt['reason'] not in REASONS[receipt['status']]):
        raise ValueError()
    if receipt['status'] in ('unavailable', 'unverified'):
        if any(receipt[field] for field in IDENTITY) or receipt['private_gateway_route'] != 'unverified':
            raise ValueError()
        return
    _identity(receipt)
    routes = ('observed', 'not_observed') if receipt['status'] == 'verified' else ('unverified',)
    if receipt['private_gateway_route'] not in routes:
        raise ValueError()
